@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
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
11
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
12
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
14
14
  <link rel="stylesheet" href="/builders/tweak-editor.css?v=2026-09-29-tw27-aqfix">
15
15
  </head>
16
16
  <!-- THE TWEAK EDITOR (task 1004321 / BV2.TW10, goal 1000095; ADR 0341 D4 and
@@ -143,14 +143,14 @@
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
145
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
146
- <script src="/builders/shell-game.js?v=2026-11-09-game-mode"></script>
146
+ <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
147
147
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
148
- <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
149
- <script src="/builders/shell.js?v=2026-11-09-game-mode"></script>
148
+ <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
149
+ <script src="/builders/shell.js?v=2026-11-11-helm"></script>
150
150
  <script src="/builders/hall-dialog.js"></script>
151
151
  <script src="/builders/palette.js" defer></script>
152
152
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
153
153
  <script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw27"></script>
154
- <script src="/builders/tweak-editor.js?v=2026-09-29-tw27-aqfix"></script>
154
+ <script src="/builders/tweak-editor.js?v=2026-11-10-options"></script>
155
155
  </body>
156
156
  </html>
@@ -647,7 +647,11 @@
647
647
  if (ev.target && ev.target.classList && ev.target.classList.contains('tw-line')) flush();
648
648
  });
649
649
  document.addEventListener('keydown', (ev) => {
650
- if (ev.key === 'Escape') { closeRefused(); setMenu(false); }
650
+ if (ev.key === 'Escape') {
651
+ // Spent only when it closed something (ADR 0399 §2.4 row 5, task 1005144).
652
+ if (!$('tw-refused').hidden || $('tw-openwrap').classList.contains('is-open')) ev.preventDefault();
653
+ closeRefused(); setMenu(false);
654
+ }
651
655
  if (ev.target && ev.target.id === 'tw-divider') {
652
656
  const next = lib.splitFromKey(st.splitSize, ev.key);
653
657
  if (next !== null) { ev.preventDefault(); setSize(next); remember(SIZE_KEY, String(st.splitSize)); }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "_": "The tweak editor (task 1004321 / BV2.TW10), built 1:1 on docs/design/mocks/tweak-mode/Editor.dc.html. Against the kit's own stub the page renders its could-not-read state (no /copy-desk feed), a state the floors must hold in. TO SEE IT POPULATED, point the kit at the hall-preview harness: `node scripts/hall-preview/server.js --port 4631 --fixture-me`, then `render.js --page '/builders/tweak-editor?page=landing:index' --base-in http://builders.localhost:4631 --base-out http://builders.localhost:4631`. The harness answers the read from fixtures/copy-desk__pages__draft.json (the real landing:index reading with the artboard's two rewrites as the saved draft), next up from copy-desk__next.json, and the autosave, the submit and the claim from its canned writes.",
3
3
  "page": "/builders/tweak-editor?page=landing:index",
4
+ "modes": ["dark"],
4
5
  "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
6
  "storage": { "session": { "hall-craft-asked": "1" } },
6
7
  "surface": "hall-ui",
@@ -38,6 +38,8 @@
38
38
  'use strict';
39
39
 
40
40
  var RUN_KEY = 'bongos.update.run';
41
+ var LATER_KEY = 'bongos.update.later';
42
+ var LATER_MS = 24 * 60 * 60 * 1000;
41
43
  var MSG = 'bongos-update'; // the tag deploy-handoff.js answers to; both sides check it
42
44
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
43
45
  // A hub door as the server words it (core-update.js updateDoor): an origin and
@@ -152,12 +154,34 @@
152
154
  return slow ? 'slow' : 'starting';
153
155
  }
154
156
 
157
+ // THE POP-UP (task 1005191). The owner, 2026-11-10: the pill alone was not seen. Game mode
158
+ // opens every page without its bars, and a project sat 3 minor versions behind. So an
159
+ // update that is ready also ASKS, once, in the hall's own dialog. Any way of closing it
160
+ // other than Update now is "later": that version is not asked about again in this browser
161
+ // for a day. The pill stays, so later never means lost.
162
+ function readLater(storage) {
163
+ var l = null;
164
+ try { l = JSON.parse((storage && storage.getItem(LATER_KEY)) || 'null'); } catch (e) { l = null; }
165
+ return l && isVersion(l.v) && isFinite(l.at) ? l : null;
166
+ }
167
+ function keepLater(storage, version, now) {
168
+ try { if (storage && isVersion(version)) storage.setItem(LATER_KEY, JSON.stringify({ v: version, at: now })); } catch (e) { /* it asks again next page */ }
169
+ }
170
+ /** Whether the pop-up opens: only for an update that is ready, and not for a version the
171
+ * owner already said "later" to inside the last day. A newer version asks again at once. */
172
+ function askDue(o) {
173
+ if (!o || o.face !== 'ready' || !isVersion(o.offered)) return false;
174
+ var l = o.later;
175
+ if (l && l.v === o.offered && o.now >= l.at && o.now - l.at < LATER_MS) return false;
176
+ return true;
177
+ }
178
+
155
179
  // ---------- the page half ----------
156
180
 
157
181
  var state = {
158
182
  api: null, update: null, held: null, sw: null, swAsked: null, swError: null, ended: null, note: null,
159
183
  face: 'none', stage: null, run: null, error: null, pill: null, card: null,
160
- popup: null, plan: null, offered: null, looks: 0, timer: null, popupTimer: null, listening: false,
184
+ popup: null, plan: null, offered: null, asked: false, looks: 0, timer: null, popupTimer: null, listening: false,
161
185
  };
162
186
 
163
187
  function doc() { return root && root.document; }
@@ -316,7 +340,7 @@
316
340
  d.body.appendChild(c);
317
341
  state.card = c;
318
342
  if (root.addEventListener) root.addEventListener('resize', function () { if (state.card && !state.card.hidden) placeCard(); });
319
- d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && state.card && !state.card.hidden) { closeCard(); if (state.pill) state.pill.focus(); } });
343
+ d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && state.card && !state.card.hidden) { e.preventDefault(); closeCard(); if (state.pill) state.pill.focus(); } }); // spent: ADR 0399 §2.4, task 1005144
320
344
  d.addEventListener('click', function (e) {
321
345
  if (!state.card || state.card.hidden) return;
322
346
  var t = e.target;
@@ -556,6 +580,69 @@
556
580
  if (root.location && root.location.reload) root.location.reload();
557
581
  }
558
582
 
583
+ // ---------- the pop-up ----------
584
+
585
+ // Not in the hub's handoff window, not on the update door itself, and not over the setup
586
+ // screen: each of those is already the thing the owner is doing.
587
+ function askAllowed() {
588
+ var d = doc();
589
+ if (!d || !root.OTBDialog || typeof root.OTBDialog.open !== 'function' || state.asked) return false;
590
+ var el = d.documentElement;
591
+ if (el && el.dataset && el.dataset.handoff === 'on') return false;
592
+ if (root.OTBSetupGate) return false;
593
+ return !(root.location && root.location.pathname === '/deploy');
594
+ }
595
+
596
+ function askBodyHtml(plan) {
597
+ var u = state.update || {};
598
+ var h = highlights((state.sw.update || {}).whats_new, HIGHLIGHTS);
599
+ var list = h.lines.length ? '<ul class="upd-card__list">' + h.lines.map(function (l) { return '<li>' + esc(l) + '</li>'; }).join('') + '</ul>' : '';
600
+ var note = plan.kind === 'cli'
601
+ ? 'This project is updated on the server that runs it: <code>bongos upgrade</code> there takes this version, and puts the old one back if it fails.'
602
+ : 'Takes a few minutes. The project restarts once, and goes back to the version it runs now on its own if the new one is not healthy.';
603
+ var versions = isVersion(u.running) && isVersion(state.offered) ? '<p class="upd-card__ver">' + mono(u.running) + ' → ' + mono(state.offered) + '</p>' : '';
604
+ return '<p>A newer version of Bongos is ready, with the latest changes and bug fixes.</p>' + list +
605
+ '<p class="upd-card__note">' + note + '</p>' + versions;
606
+ }
607
+
608
+ // Asked from the same reading the card uses, so the pop-up and the card name one version
609
+ // and the two-step rule holds: the pop-up shows the version, Update now sends that one.
610
+ function maybeAsk() {
611
+ if (!askAllowed() || !askDue({ face: state.face, offered: state.offered, later: readLater(store()), now: now() })) return;
612
+ var read = readSoftwareUpdate();
613
+ if (!read) { ask(); return; }
614
+ read.then(ask);
615
+ }
616
+ function ask() {
617
+ if (!state.sw || !askAllowed() || !askDue({ face: state.face, offered: state.offered, later: readLater(store()), now: now() })) return;
618
+ var plan = doorPlan(state.sw);
619
+ if (plan.kind === 'none') return; // a viewer the door would refuse is not asked
620
+ var d = doc();
621
+ var body = d.createElement('div');
622
+ body.className = 'upd-ask';
623
+ body.innerHTML = askBodyHtml(plan);
624
+ var canGo = plan.kind === 'local' || plan.kind === 'hub';
625
+ var offered = state.offered;
626
+ var actions = canGo
627
+ ? [{ label: 'Later', value: 'later', kind: 'ghost' }, { label: 'Update now', value: 'go' }]
628
+ : [{ label: 'Close', value: 'later', kind: 'ghost' }];
629
+ state.asked = true;
630
+ var dlg = root.OTBDialog.open({ title: 'Update the builders hall?', body: body, actions: actions, initialFocus: '[data-dialog-value="' + (actions.length - 1) + '"]' });
631
+ // Update now runs inside the press itself: a hosted project opens the hub's window, and
632
+ // a browser allows that only from the click, not from the dialog's settled promise.
633
+ if (canGo && dlg && dlg.el && dlg.el.addEventListener) {
634
+ dlg.el.addEventListener('click', function (e) {
635
+ var t = e && e.target;
636
+ var b = t && t.closest ? t.closest('[data-dialog-value]') : null;
637
+ if (!b || b.getAttribute('data-dialog-value') !== '1') return;
638
+ if (updateNow()) openCard();
639
+ });
640
+ }
641
+ if (dlg && dlg.result && dlg.result.then) {
642
+ dlg.result.then(function (v) { if (v !== 'go') keepLater(store(), offered, now()); });
643
+ }
644
+ }
645
+
559
646
  /** shell.js hands over GET /core-update's answer; the pill decides what, if anything, to show. */
560
647
  function mount(data, opts) {
561
648
  state.api = opts && opts.api;
@@ -575,6 +662,7 @@
575
662
  state.face = face;
576
663
  drawPill();
577
664
  if (face === 'running') schedule();
665
+ if (face === 'ready') maybeAsk();
578
666
  return face;
579
667
  }
580
668
 
@@ -590,6 +678,7 @@
590
678
  // the pure half, for the tests
591
679
  highlights: highlights, doorPlan: doorPlan, pillState: pillState, stageOf: stageOf,
592
680
  readRun: readRun, keepRun: keepRun, forgetRun: forgetRun, isVersion: isVersion,
681
+ askDue: askDue, readLater: readLater, keepLater: keepLater, LATER_KEY: LATER_KEY, LATER_MS: LATER_MS,
593
682
  RUN_KEY: RUN_KEY, MSG: MSG, SLOW_MS: SLOW_MS, GIVE_UP_MS: GIVE_UP_MS,
594
683
  _state: state,
595
684
  };
@@ -0,0 +1,80 @@
1
+ /* visor.css — the first-person visor (task 1005148, visor.js; ADR 0399). Glass over the scene:
2
+ the frame lines, the place, three pills and the actions sheet. It shows in game mode only, and
3
+ it is an overlay, never layout, so the stage under it never changes size (ADR 0399 §2.1).
4
+ Colours are the hall's tokens; the frame is hairline ink, never a glow (DESIGN.md: no
5
+ near-black-plus-neon HUD). Record: modules/hall-ui/records/visor.md. */
6
+ .visor { display: none; }
7
+ html[data-game="on"] .visor:not([hidden]) {
8
+ display: block; position: fixed; inset: 0; z-index: 4; pointer-events: none;
9
+ /* where the glass sits, so a scene's pop-up can keep clear of it (ADR 0399 §2.3) */
10
+ --visor-inset-top: 64px; --visor-inset-right: 40px; --visor-inset-bottom: 76px; --visor-inset-left: 40px;
11
+ color: var(--ink); font-family: var(--font-body);
12
+ }
13
+
14
+ /* the frame: stretched over the stage, every stroke one hairline at any size */
15
+ .visor__frame { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
16
+ .visor__frame path { vector-effect: non-scaling-stroke; }
17
+ .visor__line { fill: none; stroke: var(--rule); stroke-width: var(--ds-line); }
18
+ .visor__line--side { stroke: var(--rule-soft); }
19
+ .visor__cell { fill: color-mix(in srgb, var(--ink) 12%, transparent); stroke: var(--rule); stroke-width: var(--ds-line); }
20
+ .visor__mark { fill: var(--accent); stroke: none; }
21
+
22
+ /* the place, under the compass */
23
+ .visor__place {
24
+ position: absolute; left: 50%; top: max(9.5%, 84px); transform: translateX(-50%); margin: 0;
25
+ font-family: var(--font-mono); font-size: var(--ds-text-2xs); letter-spacing: .14em; text-transform: uppercase;
26
+ color: var(--ink-soft); white-space: nowrap;
27
+ }
28
+
29
+ /* the pills: the only parts of the glass that take a pointer */
30
+ .visor__pill, .visor__me {
31
+ position: absolute; pointer-events: auto; box-sizing: border-box;
32
+ display: inline-flex; align-items: center; gap: var(--ds-space-xs);
33
+ min-height: 46px; padding: 0 var(--ds-space-sm);
34
+ background: var(--bg-card); color: var(--ink); border: var(--ds-line) solid var(--rule);
35
+ border-radius: var(--ds-radius-pill); font: inherit; font-size: var(--ds-text-sm); text-decoration: none;
36
+ }
37
+ .visor__pill { cursor: pointer; }
38
+ .visor__pill:hover { border-color: var(--ink-faint); }
39
+ .visor__pill:focus-visible, .visor__row:focus-visible, .visor__close:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
40
+ .visor__pill--main { left: var(--visor-inset-left); bottom: var(--visor-inset-bottom); background: var(--accent); color: var(--accent-on); border-color: var(--accent); font-weight: 500; }
41
+ .visor__pill--main:hover { border-color: var(--accent-deep); }
42
+ .visor__pill--actions { right: var(--visor-inset-right); top: var(--visor-inset-top); }
43
+ .visor__pill[hidden] { display: none; }
44
+ .visor__dots { width: 14px; height: 4px; background: radial-gradient(circle, currentColor 1.5px, transparent 2px) 0 0 / 5px 4px repeat-x; }
45
+ .visor__me { right: var(--visor-inset-right); bottom: var(--visor-inset-bottom); padding: 4px var(--ds-space-sm) 4px 4px; pointer-events: none; }
46
+ .visor__sprite { width: 38px; height: 38px; border-radius: var(--ds-radius-pill); object-fit: cover; object-position: 50% 12%; background: var(--bg-deep); }
47
+ .visor__who { display: flex; flex-direction: column; line-height: 1.2; }
48
+ .visor__name { font-weight: 500; }
49
+ .visor__credits { font-size: var(--ds-text-2xs); color: var(--ink-soft); font-variant-numeric: tabular-nums; }
50
+ .visor__key {
51
+ font-family: var(--font-mono); font-size: var(--ds-text-2xs); line-height: 1; padding: 3px 5px;
52
+ border: var(--ds-line) solid currentColor; border-radius: var(--ds-radius-pill); opacity: .75;
53
+ }
54
+
55
+ /* the sheet: above the scene's pop-ups, under the modals (ADR 0399 §2.3 layer 4) */
56
+ .visor__sheet {
57
+ position: absolute; right: var(--visor-inset-right); top: var(--visor-inset-top); z-index: 1;
58
+ pointer-events: auto; box-sizing: border-box; width: min(18rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
59
+ max-height: calc(100% - var(--visor-inset-top) - var(--visor-inset-bottom)); overflow-y: auto;
60
+ padding: var(--ds-space-sm); background: var(--bg-card); border: var(--ds-line) solid var(--rule);
61
+ border-radius: var(--ds-radius-panel); box-shadow: var(--ds-shadow-pop);
62
+ }
63
+ .visor__sheet-head { display: flex; align-items: center; justify-content: space-between; }
64
+ .visor__sheet-title { margin: 0; font-size: var(--ds-text-md); font-weight: 500; }
65
+ .visor__close { min-width: 32px; min-height: 32px; background: none; border: 0; color: var(--ink-soft); cursor: pointer; }
66
+ .visor__cap { margin: var(--ds-space-xs) 0 2px; font-family: var(--font-mono); font-size: var(--ds-text-2xs); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-soft); }
67
+ .visor__rows { list-style: none; margin: 0; padding: 0; }
68
+ .visor__rows + .visor__rows { margin-top: var(--ds-space-xs); }
69
+ .visor__row {
70
+ display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-xs);
71
+ box-sizing: border-box; width: 100%; min-height: 36px; padding: 0 2px;
72
+ background: none; border: 0; border-top: var(--ds-line) solid var(--rule-soft);
73
+ color: var(--ink); font: inherit; font-size: var(--ds-text-sm); text-align: left; text-decoration: none; cursor: pointer;
74
+ }
75
+ .visor__row:hover { color: var(--accent-deep); }
76
+ .visor__row--hint { cursor: default; color: var(--ink-soft); }
77
+ .visor__row--hint:hover { color: var(--ink-soft); }
78
+
79
+ /* a phone (640px and narrower): the rules live in style.css’s shared phone block, so the
80
+ visor adds no breakpoint of its own (the design-count ratchet). */
@@ -0,0 +1,316 @@
1
+ // visor.js — the first-person visor: glass over the scene (task 1005148, BONGOS-V3 R08,
2
+ // goal 1000142, criterion `you-are-a-character-in-first-person`). Built to ADR 0399.
3
+ //
4
+ // What is on the glass (the design the owner agreed on 2026-11-10, task 1005148):
5
+ // - the frame: thin curved visor lines at the top, the bottom and both sides, a small
6
+ // centre cross, and a compass of nine slanted cells along the top line with the one
7
+ // salmon marker under its centre. Drawn once as SVG; it is ornament, never a control.
8
+ // - the place, in a mono line under the compass ("THE SKY", "GOVERNOR CITY", …).
9
+ // - bottom-left: your character's main action, the one accent pill, with its key.
10
+ // - bottom-right: your sprite (third person) with your character's name and your credits
11
+ // (builders.total_credits — credits only, never a payout; the spec's "does not exist").
12
+ // - top-right: "Actions" (key A), which opens the sheet: your character's actions, then
13
+ // the keys of the place you are in, then Options on Escape.
14
+ //
15
+ // The rules it keeps (ADR 0399):
16
+ // - one overlay, #visor, with pointer-events: none; only its pills take a pointer, so a
17
+ // drag, a wheel or a click on the world reaches the scene unchanged (§2.1).
18
+ // - no camera: a scene registers itself once with OTBVisor.scene({ name, place, actions,
19
+ // keys }) and a movement action calls the scene's own function (§2.2). A page with no
20
+ // scene registered shows no visor.
21
+ // - Escape (§2.4): an OPEN SHEET takes the press in capture and stops it (row 4), so the
22
+ // star under it stays picked; the ambient glass never takes one, so the last press
23
+ // still reaches the options menu (shell-game.js onEscape).
24
+ // - it hides nothing and gates nothing (§2.5): every action is a link or tool a page
25
+ // already offers, and the server's rank checks answer exactly as before.
26
+ // - game mode only: html[data-game="on"] shows it (visor.css). Outside it, the hall is
27
+ // exactly as it was.
28
+ //
29
+ // The character comes from craft-mode.js (task 1005147): the held character, or this
30
+ // session's mode on a core that has no characters yet. With neither, no visor.
31
+ // R09 (task 1005149) turns the keys below into the one rebindable registry.
32
+ (function () {
33
+ 'use strict';
34
+
35
+ // ---------- each character's actions ----------
36
+ // `href` opens a page the hall already has; `tool` opens one of craft-mode's tools on
37
+ // this page. The first is the character's main action (the accent pill).
38
+ var ACTIONS = {
39
+ artisan: [
40
+ { id: 'claim', label: 'Claim a task', key: 'c', href: '/work' },
41
+ { id: 'blockers', label: 'Blockers', key: 'b', href: '/blockers' },
42
+ { id: 'sky', label: 'Goal sky', key: 'g', href: '/goal-map' },
43
+ { id: 'thinking', label: 'Your thinking', key: 't', href: '/thinking' },
44
+ ],
45
+ artist: [
46
+ { id: 'review', label: 'Review this page', key: 'r', tool: 'palette' },
47
+ { id: 'words', label: 'Edit its words', key: 'e', tool: 'paintbrush' },
48
+ { id: 'idea', label: 'Write an idea', key: 'i', tool: 'lightbulb' },
49
+ { id: 'studio', label: 'Studio', key: 's', href: '/studio' },
50
+ ],
51
+ patron: [
52
+ { id: 'board', label: 'Board room', key: 'o', href: '/board-room' },
53
+ { id: 'city', label: 'Governor city', key: 'v', href: '/city' },
54
+ { id: 'blockers', label: 'Blockers', key: 'b', href: '/blockers' },
55
+ { id: 'watch', label: 'Watch', key: 'w', href: '/watch' },
56
+ ],
57
+ };
58
+ var SHEET_KEY = 'a';
59
+ var NAMES = { artisan: 'Artisan', artist: 'Artist', patron: 'Patron' };
60
+
61
+ function esc(v) { return window.OTB && window.OTB.escapeHtml ? window.OTB.escapeHtml(v) : String(v == null ? '' : v).replace(/[^\w .,:'-]/g, ''); }
62
+ function craft() { return window.OTBCraftMode || null; }
63
+ // The character on the glass: the held one, else this session's mode, else none.
64
+ function character() {
65
+ var C = craft();
66
+ if (!C) return null;
67
+ var held = typeof C.character === 'function' ? C.character() : undefined;
68
+ if (typeof held === 'string' && ACTIONS[held]) return held;
69
+ var mode = typeof C.current === 'function' ? C.current() : null;
70
+ return ACTIONS[mode] ? mode : null;
71
+ }
72
+ function actionsFor(id) { return ACTIONS[id] ? ACTIONS[id].slice() : []; }
73
+ function credits() {
74
+ var C = craft();
75
+ var me = C && typeof C.me === 'function' ? C.me() : null;
76
+ var n = me && me.builder ? Number(me.builder.total_credits) : NaN;
77
+ return isFinite(n) ? n : null;
78
+ }
79
+ function creditsText(n) {
80
+ if (n == null) return '';
81
+ var v = Math.round(n);
82
+ var s = String(Math.abs(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
83
+ return (v < 0 ? '−' : '') + s + (Math.abs(v) === 1 ? ' credit' : ' credits');
84
+ }
85
+ // A link on the hall's own host: '/work' on the builders host, '/builders/work' on the apex.
86
+ function hallHref(p) {
87
+ var here = String((window.location && window.location.pathname) || '');
88
+ return (here.indexOf('/builders/') === 0 || here === '/builders' ? '/builders' : '') + p;
89
+ }
90
+
91
+ // ---------- the scene ----------
92
+ var scene = null;
93
+ // A scene says what it is and, if it has a camera, the actions that move it (ADR 0399
94
+ // §2.2). `keys` are lines for the sheet's "this place" list: keys the scene already binds.
95
+ function registerScene(s) {
96
+ if (!s || typeof s !== 'object') return;
97
+ scene = {
98
+ name: String(s.name || ''),
99
+ place: String(s.place || ''),
100
+ actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; }),
101
+ keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
102
+ };
103
+ render();
104
+ }
105
+
106
+ // ---------- drawing ----------
107
+ var root = null, sheet = null, returnFocus = null;
108
+ var NS = 'http://www.w3.org/2000/svg';
109
+ // The frame is drawn in a 1000×1000 box stretched over the stage (preserveAspectRatio
110
+ // none), with non-scaling strokes so every line stays one hairline at any size.
111
+ function frameSvg() {
112
+ var cells = '';
113
+ for (var i = -4; i <= 4; i++) {
114
+ var x = 500 + i * 38, y = 30 + Math.pow(i / 4, 2) * 14, s = i * 2;
115
+ cells += '<path class="visor__cell" d="M' + (x - 15) + ' ' + (y + 4 - s / 2) + 'l30 ' + s + 'l-6 15l-30 ' + (-s) + 'z"/>';
116
+ }
117
+ return '<svg class="visor__frame" viewBox="0 0 1000 1000" preserveAspectRatio="none" aria-hidden="true" focusable="false" xmlns="' + NS + '">' +
118
+ '<path class="visor__line" d="M0 120Q500 -10 1000 120"/>' +
119
+ '<path class="visor__line" d="M0 880Q500 1010 1000 880"/>' +
120
+ '<path class="visor__line visor__line--side" d="M18 120Q4 500 18 880M982 120Q996 500 982 880"/>' +
121
+ '<path class="visor__line" d="M490 500h-12M510 500h12M500 490v-14M500 510v14"/>' +
122
+ cells + '<path class="visor__mark" d="M494 76l6 9l6 -9z"/>' +
123
+ '</svg>';
124
+ }
125
+ function keyHtml(k) { k = String(k); return '<kbd class="visor__key">' + esc(k.length === 1 ? k.toUpperCase() : k) + '</kbd>'; }
126
+ function actionAttrs(a) {
127
+ return a.href ? 'href="' + esc(hallHref(a.href)) + '"' : 'href="#" role="button"';
128
+ }
129
+ function glassHtml(id) {
130
+ var acts = actionsFor(id), main = acts[0], C = craft();
131
+ var sprite = C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '';
132
+ var n = credits();
133
+ return frameSvg() +
134
+ (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
135
+ '<a class="visor__pill visor__pill--main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
136
+ '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
137
+ '<button class="visor__pill visor__pill--actions" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
138
+ '<span class="visor__dots" aria-hidden="true"></span><span>Actions</span>' + keyHtml(SHEET_KEY) + '</button>' +
139
+ '<div class="visor__me">' +
140
+ (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
141
+ '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
142
+ '<span class="visor__credits" data-visor-credits>' + esc(creditsText(n)) + '</span></span>' +
143
+ '</div>';
144
+ }
145
+ function sheetHtml(id) {
146
+ var rows = actionsFor(id).map(function (a) {
147
+ return '<li><a class="visor__row" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '><span>' + esc(a.label) + '</span>' + keyHtml(a.key) + '</a></li>';
148
+ }).join('');
149
+ var place = scene.actions.map(function (a, i) {
150
+ return '<li><button class="visor__row" type="button" data-visor-scene="' + i + '"><span>' + esc(a.label) + '</span>' + (a.key ? keyHtml(a.key) : '') + '</button></li>';
151
+ }).concat(scene.keys.map(function (k) {
152
+ return '<li><span class="visor__row visor__row--hint"><span>' + esc(k.label) + '</span>' + keyHtml(k.key) + '</span></li>';
153
+ })).join('');
154
+ return '<div class="visor__sheet-head"><h2 class="visor__sheet-title" id="visor-sheet-title">Actions</h2>' +
155
+ '<button class="visor__close" type="button" data-visor-close aria-label="Close actions">' + keyHtml('Esc') + '</button></div>' +
156
+ '<p class="visor__cap">' + esc(NAMES[id]) + '</p><ul class="visor__rows">' + rows + '</ul>' +
157
+ (place ? '<p class="visor__cap">This place</p><ul class="visor__rows">' + place + '</ul>' : '') +
158
+ '<ul class="visor__rows"><li><a class="visor__row" href="' + esc(hallHref('/settings')) + '"><span>Options</span>' + keyHtml('Esc') + '</a></li></ul>';
159
+ }
160
+ function shown() { return !!(root && !root.hidden); }
161
+ function render() {
162
+ if (!document.body) return;
163
+ var id = scene && !embedded() ? character() : null;
164
+ var html = document.documentElement;
165
+ if (!id) { if (html.classList) html.classList.remove('visor-on'); if (root) { closeSheet(false); root.hidden = true; } return; }
166
+ // A scene's own layers read this to keep clear of the glass (thinking.css), in game mode only.
167
+ if (html.classList) html.classList.toggle('visor-on', html.dataset.game === 'on');
168
+ if (!root) {
169
+ root = document.createElement('div');
170
+ root.id = 'visor';
171
+ root.className = 'visor';
172
+ root.setAttribute('role', 'region');
173
+ root.setAttribute('aria-label', 'Visor');
174
+ root.addEventListener('click', onClick);
175
+ document.body.appendChild(root);
176
+ }
177
+ var open = !!sheet;
178
+ closeSheet(false);
179
+ root.hidden = false;
180
+ root.setAttribute('data-character', id);
181
+ root.innerHTML = glassHtml(id);
182
+ if (open) openSheet();
183
+ }
184
+ function embedded() {
185
+ var G = window.OTBShellGame;
186
+ return !!(G && typeof G.embedded === 'function' && G.embedded());
187
+ }
188
+
189
+ // ---------- the sheet ----------
190
+ function openSheet() {
191
+ var id = character();
192
+ if (!shown() || sheet || !id) return;
193
+ returnFocus = document.activeElement;
194
+ sheet = document.createElement('div');
195
+ sheet.id = 'visor-sheet';
196
+ sheet.className = 'visor__sheet';
197
+ sheet.setAttribute('role', 'dialog');
198
+ sheet.setAttribute('aria-labelledby', 'visor-sheet-title');
199
+ sheet.innerHTML = sheetHtml(id);
200
+ sheet.addEventListener('click', onClick);
201
+ root.appendChild(sheet);
202
+ var btn = root.querySelector('[data-visor-sheet]');
203
+ if (btn) { btn.setAttribute('aria-expanded', 'true'); btn.hidden = true; }
204
+ document.addEventListener('keydown', onSheetKey, true);
205
+ var first = sheet.querySelector('.visor__rows a, .visor__rows button');
206
+ if (first && first.focus) first.focus();
207
+ }
208
+ function closeSheet(refocus) {
209
+ if (!sheet) return;
210
+ document.removeEventListener('keydown', onSheetKey, true);
211
+ if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
212
+ sheet = null;
213
+ var btn = root && root.querySelector('[data-visor-sheet]');
214
+ if (btn) { btn.setAttribute('aria-expanded', 'false'); btn.hidden = false; }
215
+ if (refocus !== false) {
216
+ var to = returnFocus && returnFocus.isConnected !== false && returnFocus !== document.body ? returnFocus : btn;
217
+ if (to && to.focus) { try { to.focus(); } catch (_) { /* gone */ } }
218
+ }
219
+ returnFocus = null;
220
+ }
221
+ // ADR 0399 §2.4 row 4: capture on document, and stop the press, so no scene listener
222
+ // below (the sky's unpick) and not the options menu also spends it.
223
+ function onSheetKey(e) {
224
+ if (!sheet || e.key !== 'Escape') return;
225
+ e.preventDefault();
226
+ e.stopPropagation();
227
+ closeSheet(true);
228
+ }
229
+
230
+ // ---------- running an action ----------
231
+ function run(a) {
232
+ if (!a) return;
233
+ if (a.tool) {
234
+ var C = craft();
235
+ if (C && typeof C.openTool === 'function') C.openTool(a.tool);
236
+ return;
237
+ }
238
+ if (a.href) window.location.assign(hallHref(a.href));
239
+ }
240
+ function find(id) {
241
+ var acts = actionsFor(character());
242
+ for (var i = 0; i < acts.length; i++) if (acts[i].id === id) return acts[i];
243
+ return null;
244
+ }
245
+ function onClick(e) {
246
+ var t = e.target && e.target.closest ? e.target : null;
247
+ if (!t) return;
248
+ if (t.closest('[data-visor-close]')) { closeSheet(true); return; }
249
+ if (t.closest('[data-visor-sheet]')) { openSheet(); return; }
250
+ var sc = t.closest('[data-visor-scene]');
251
+ if (sc && scene) { var a = scene.actions[Number(sc.getAttribute('data-visor-scene'))]; closeSheet(false); if (a) a.run(); return; }
252
+ var act = t.closest('[data-visor-act]');
253
+ if (!act) return;
254
+ var found = find(act.getAttribute('data-visor-act'));
255
+ if (found && found.tool) { e.preventDefault(); closeSheet(false); run(found); }
256
+ else closeSheet(false); // a link: the browser follows it
257
+ }
258
+
259
+ // ---------- the keys ----------
260
+ function inField(el) {
261
+ var tag = el && el.tagName ? String(el.tagName).toLowerCase() : '';
262
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || !!(el && el.isContentEditable);
263
+ }
264
+ // Something above the glass has the keyboard: the character picker, a modal dialog, the
265
+ // palette, the start screen. The visor's keys wait while any of them is up.
266
+ function busy() {
267
+ var html = document.documentElement;
268
+ if (html.classList && (html.classList.contains('craft-picker-open') || html.classList.contains('gs-open'))) return true;
269
+ return !!document.querySelector('dialog[open], [aria-modal="true"]');
270
+ }
271
+ // Which action (or the sheet) a key press means, or null. Pure, for the tests.
272
+ function keyFor(id, e) {
273
+ if (!e || e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey || typeof e.key !== 'string' || e.key.length !== 1) return null;
274
+ var k = e.key.toLowerCase();
275
+ if (k === SHEET_KEY) return 'sheet';
276
+ var acts = actionsFor(id);
277
+ for (var i = 0; i < acts.length; i++) if (acts[i].key === k) return acts[i];
278
+ return null;
279
+ }
280
+ // Bubble, on window: a scene or pop-up that used the key has called preventDefault by now.
281
+ function onKey(e) {
282
+ if (!shown() || inField(e.target) || busy()) return;
283
+ var hit = keyFor(character(), e);
284
+ if (!hit) return;
285
+ e.preventDefault();
286
+ if (hit === 'sheet') { if (sheet) closeSheet(true); else openSheet(); return; }
287
+ closeSheet(false);
288
+ run(hit);
289
+ }
290
+
291
+ function start() {
292
+ window.addEventListener('keydown', onKey);
293
+ // A scene whose script ran before this file left its registration here.
294
+ if (window.__visorScene && !scene) registerScene(window.__visorScene);
295
+ // The character and the credits settle when /me answers (craft-mode.js), after this file.
296
+ document.addEventListener('otbcraft:me', render);
297
+ document.addEventListener('otb:craft', render);
298
+ render();
299
+ }
300
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
301
+ else start();
302
+
303
+ window.OTBVisor = {
304
+ scene: registerScene,
305
+ render: render,
306
+ open: openSheet,
307
+ close: function () { closeSheet(true); },
308
+ isOpen: function () { return !!sheet; },
309
+ // pure parts, for the tests
310
+ ACTIONS: ACTIONS,
311
+ actionsFor: actionsFor,
312
+ keyFor: keyFor,
313
+ creditsText: creditsText,
314
+ character: character,
315
+ };
316
+ })();