@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.
- package/.bongos-core.json +554 -394
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
- package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
- package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
- package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
- package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +195 -3
- package/docs/api-reference.md +8 -4
- package/docs/architecture.md +7 -3
- package/docs/copy-inventory.md +393 -378
- package/docs/copy-registry.json +722 -578
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +5 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/{engineer.md → artisan.md} +5 -5
- package/docs/packs/artist.md +50 -4
- package/docs/page-inventory.json +16 -7
- package/docs/page-readings.json +3184 -3338
- package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
- package/migrations/core_296_crafts_artisan_artist.sql +72 -0
- package/migrations/core_297_builder_character.sql +30 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/economy/credits-by-craft.js +29 -15
- package/modules/hall-ui/public/ai-studio.html +6 -4
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/board-room.states.json +1 -0
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/city.css +3 -4
- package/modules/hall-ui/public/city.html +9 -9
- package/modules/hall-ui/public/city.js +17 -56
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/deploy.states.json +1 -0
- package/modules/hall-ui/public/diagram-viewer.js +2 -1
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/goal-map.html +9 -6
- package/modules/hall-ui/public/goals.html +5 -5
- package/modules/hall-ui/public/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/government.states.json +1 -0
- package/modules/hall-ui/public/hall-render.js +3 -1
- package/modules/hall-ui/public/home-city-draw.js +3 -3
- package/modules/hall-ui/public/home-city.css +4 -0
- package/modules/hall-ui/public/home-city.js +17 -9
- package/modules/hall-ui/public/home-gallery.js +8 -1
- package/modules/hall-ui/public/home-idea-box.css +2 -2
- package/modules/hall-ui/public/home-idea-box.js +3 -3
- package/modules/hall-ui/public/idea.html +5 -5
- package/modules/hall-ui/public/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- package/modules/hall-ui/public/market-watch.html +6 -9
- package/modules/hall-ui/public/market-watch.js +22 -45
- package/modules/hall-ui/public/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- package/modules/hall-ui/public/options-menu.js +409 -0
- package/modules/hall-ui/public/paintbrush.js +3 -3
- package/modules/hall-ui/public/palette.js +11 -13
- package/modules/hall-ui/public/primer.html +5 -5
- package/modules/hall-ui/public/primer.states.json +1 -0
- package/modules/hall-ui/public/profile-crafts.js +1 -1
- package/modules/hall-ui/public/profile-roles.js +23 -19
- package/modules/hall-ui/public/profile.css +1 -1
- package/modules/hall-ui/public/profile.html +19 -18
- package/modules/hall-ui/public/profile.js +19 -18
- package/modules/hall-ui/public/profile.states.json +5 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/roster.js +3 -3
- package/modules/hall-ui/public/roster.states.json +1 -0
- package/modules/hall-ui/public/settings.css +24 -4
- package/modules/hall-ui/public/settings.html +21 -17
- package/modules/hall-ui/public/settings.js +44 -44
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +110 -0
- package/modules/hall-ui/public/shell-game.js +316 -3
- package/modules/hall-ui/public/shell.js +15 -66
- package/modules/hall-ui/public/sky-interaction.js +7 -0
- package/modules/hall-ui/public/speciality-tools.js +1 -2
- package/modules/hall-ui/public/street-card.states.json +1 -0
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +6 -10
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +197 -112
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/thinking.states.json +1 -0
- package/modules/hall-ui/public/tweak-editor.html +5 -5
- package/modules/hall-ui/public/tweak-editor.js +5 -1
- package/modules/hall-ui/public/tweak-editor.states.json +1 -0
- package/modules/hall-ui/public/update-pill.js +91 -2
- package/modules/hall-ui/public/visor.css +80 -0
- package/modules/hall-ui/public/visor.js +316 -0
- package/modules/hall-ui/public/watch-repo-invite.js +159 -0
- package/modules/hall-ui/public/watch.html +6 -5
- package/modules/hall-ui/public/watch.js +17 -2
- package/modules/hall-ui/public/watch.states.json +1 -0
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/public/work.states.json +1 -0
- package/modules/hall-ui/records/builders-roster.md +2 -2
- package/modules/hall-ui/records/craft-mode.md +13 -11
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/governor-street.md +1 -1
- package/modules/hall-ui/records/home-city.md +3 -1
- package/modules/hall-ui/records/market-watch-settings.md +2 -0
- package/modules/hall-ui/records/options-menu.md +15 -0
- package/modules/hall-ui/records/profile-roles.md +11 -11
- package/modules/hall-ui/records/sky.md +3 -3
- package/modules/hall-ui/records/start-screen.md +25 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/hall-ui/records/visor.md +23 -0
- package/modules/hall-ui/records/welcome-tower.md +5 -5
- package/modules/hall-ui/specimens/components.states.json +2 -1
- package/modules/ideas/ideator-stats.js +3 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/activity.js +1 -1
- package/modules/lifecycle/cascade.js +4 -4
- package/modules/lifecycle/goal-spec-task.js +1 -1
- package/modules/lifecycle/help-requests.js +19 -15
- package/modules/lifecycle/kickoff-checklist.js +3 -3
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
- package/modules/lifecycle/mingle.js +4 -4
- package/modules/lifecycle/page-tweak-write.js +1 -1
- package/modules/lifecycle/routes/task-write-routes.js +3 -1
- package/modules/lifecycle/routes/tasks.js +3 -3
- package/modules/lifecycle/ship-card.js +2 -3
- package/modules/lifecycle/task-classifier.js +39 -29
- package/modules/lifecycle/tests/cascade.mjs +7 -7
- package/modules/onboarding/founding-mode.js +3 -0
- package/modules/onboarding/genesis-home.js +21 -1
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/page-notes/notes.js +3 -2
- package/modules/page-notes/split.js +1 -1
- package/modules/platform-identity/craft-rollup.js +35 -7
- package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/community-leaderboard.js +2 -1
- package/modules/platform-identity/routes/community-search.js +2 -1
- package/modules/platform-identity/routes/sso.js +2 -1
- package/modules/platform-identity/tests/platform-identity.mjs +6 -6
- package/modules/public-landing/public/community.html +3 -5
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
- package/modules/specialities/routes/specialities.js +2 -2
- package/modules/status-ui/public/status.js +1 -1
- package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
- package/modules/ui-design/kit/fixtures/community-search.json +2 -2
- package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
- package/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -0
- package/scripts/bongos/autobongos-loop.js +1 -1
- package/scripts/bongos/discipline-modes.json +7 -12
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +7 -7
- package/scripts/bongos/grade-smoke.js +2 -2
- package/scripts/bongos/init.js +2 -2
- package/scripts/bongos/newcomer-floor.config.json +1 -2
- package/scripts/bongos/newcomer-floor.js +4 -3
- package/scripts/bongos/newcomer-floor.templates.json +10 -12
- package/scripts/bongos/newcomer-floor.test.js +47 -38
- package/scripts/bongos/role-pack-guard.js +5 -4
- package/scripts/bongos/seed-tasks.example.json +3 -3
- package/scripts/bongos/setup.js +20 -107
- package/scripts/bongos/start.js +2 -2
- package/scripts/bongos/task.js +5 -4
- package/scripts/bongos/triage.js +1 -1
- package/scripts/bongos/worktree.js +16 -4
- package/scripts/hall-preview/README.md +2 -3
- package/scripts/setup-builder.js +6 -46
- package/src/bongos/auth-admission.js +8 -8
- package/src/bongos/craft-mode.js +2 -0
- package/src/bongos/db.js +39 -1
- package/src/bongos/github-repos.js +78 -0
- package/src/bongos/module-scope-map.js +22 -0
- package/src/bongos/role-stats.js +27 -11
- package/src/bongos/routes/github.js +100 -2
- package/src/bongos/routes/me.js +33 -0
- package/src/bongos/serve-internal.js +33 -11
- package/src/crafts.js +66 -0
- package/src/hall-helm.js +79 -0
- package/src/module-api.js +6 -1
- package/src/module-loader/manifest-schema.js +16 -0
- package/src/modules.js +2 -2
- package/src/role-responsibilities.js +6 -5
- package/tests/acclaim_feed_evidence_db.mjs +1 -1
- package/tests/activity_rollup_as_of.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +3 -3
- package/tests/application_lifecycle.mjs +1 -1
- package/tests/artist_review.mjs +1 -1
- package/tests/artist_review_goal.mjs +1 -1
- package/tests/blocker_hall_live_proof.mjs +1 -1
- package/tests/blocker_tasks_named_db.mjs +2 -2
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/cap_newcomer_tasks.mjs +13 -13
- package/tests/character_creation.mjs +294 -0
- package/tests/claim_gate_open_blocker.mjs +2 -2
- package/tests/claim_gate_rebase.mjs +6 -6
- package/tests/claude_materialize.mjs +1 -1
- package/tests/clawback_confirmed_backlog.mjs +3 -3
- package/tests/collab_page.mjs +8 -9
- package/tests/collab_self_settling.mjs +2 -2
- package/tests/community_leaderboard_route.mjs +13 -5
- package/tests/community_proof_db.mjs +3 -0
- package/tests/community_search_route.mjs +7 -6
- package/tests/conductor.mjs +1 -1
- package/tests/conductor_main_e2e.mjs +8 -8
- package/tests/copy_desk_flags_db.mjs +1 -1
- package/tests/copy_desk_page_approve.mjs +2 -2
- package/tests/copy_desk_page_claim.mjs +2 -2
- package/tests/copy_desk_page_next.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +2 -2
- package/tests/core_258_box_session_claims_db.mjs +1 -1
- package/tests/craft_mode.mjs +48 -34
- package/tests/craft_mode_home.mjs +20 -20
- package/tests/craft_mode_looks.mjs +16 -15
- package/tests/craft_mode_nav.mjs +31 -29
- package/tests/craft_review_mode.mjs +46 -41
- package/tests/crafts_vocabulary.mjs +70 -0
- package/tests/cross_discipline_priority_direction.mjs +5 -5
- package/tests/design_reference.mjs +1 -1
- package/tests/fitness.mjs +7 -11
- package/tests/funded_project_pays_proof_db.mjs +1 -1
- package/tests/genesis_home.mjs +21 -1
- package/tests/github_chip.mjs +3 -3
- package/tests/github_repo_invite.mjs +379 -0
- package/tests/goal_criterion_confirm_ui.mjs +1 -1
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/goal_spec_task.mjs +1 -1
- package/tests/goal_spec_task_db.mjs +3 -3
- package/tests/grade_engine.mjs +2 -2
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_prompt_window_bound.mjs +1 -1
- package/tests/grader_reader_lens.mjs +2 -2
- package/tests/grader_severity_gate.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/hall_approval_queue.mjs +1 -1
- package/tests/hall_board_world.mjs +2 -2
- package/tests/hall_builders_page_pass.mjs +3 -2
- package/tests/hall_city.mjs +16 -30
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_helm.mjs +187 -0
- package/tests/hall_kit_charts.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_my_agents.mjs +16 -16
- package/tests/hall_nav.mjs +22 -15
- package/tests/hall_options_menu.mjs +247 -0
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_palette.mjs +8 -9
- package/tests/hall_profile_world.mjs +3 -3
- package/tests/hall_record_world.mjs +6 -4
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +307 -0
- package/tests/hall_street.mjs +15 -28
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_topbar_320.mjs +4 -2
- package/tests/hall_visor.mjs +224 -0
- package/tests/help_requests.mjs +47 -15
- package/tests/helpers.mjs +4 -4
- package/tests/home_city.mjs +6 -4
- package/tests/home_city_widget.mjs +33 -14
- package/tests/home_gallery_widget.mjs +4 -4
- package/tests/home_idea_box.mjs +6 -6
- package/tests/hub_community_page.mjs +3 -3
- package/tests/hub_craft_rollup.mjs +75 -36
- package/tests/hub_recent_ships.mjs +3 -2
- package/tests/idea_cascade.mjs +2 -2
- package/tests/idea_credit_streams_db.mjs +1 -1
- package/tests/idea_detail_page.mjs +1 -1
- package/tests/idea_ideate_full.mjs +5 -2
- package/tests/idea_spark_hall.mjs +1 -1
- package/tests/init.mjs +3 -3
- package/tests/instance_manifest.mjs +2 -2
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/leaderboard_roles.mjs +34 -25
- package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
- package/tests/lightbulb.mjs +14 -10
- package/tests/main_discipline.mjs +8 -8
- package/tests/markdown_line_endings.mjs +1 -1
- package/tests/market_watch_pages.mjs +52 -74
- package/tests/me_streak_leaderboard_queries.mjs +3 -3
- package/tests/migration_allocation.mjs +1 -1
- package/tests/mingle.mjs +36 -21
- package/tests/module-scope-map.mjs +22 -2
- package/tests/module_api.mjs +1 -0
- package/tests/module_contributions.mjs +13 -13
- package/tests/module_manifest.mjs +29 -0
- package/tests/module_skills_hook.mjs +2 -2
- package/tests/modules.mjs +14 -7
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/needs_rebase.mjs +1 -1
- package/tests/newcomer_restock.mjs +13 -13
- package/tests/page_notes_panel.mjs +22 -22
- package/tests/page_tweak_no_block.mjs +1 -1
- package/tests/paintbrush.mjs +7 -7
- package/tests/privacy_matrix.mjs +1 -0
- package/tests/profile_activity.mjs +20 -23
- package/tests/profile_earnings.mjs +1 -1
- package/tests/profile_nudge_prefs.mjs +4 -4
- package/tests/profile_role_headline.mjs +73 -46
- package/tests/profile_role_stats.mjs +55 -45
- package/tests/profile_rollup_consent.mjs +19 -6
- package/tests/profile_route.mjs +12 -12
- package/tests/profile_ui_cross_project.mjs +4 -4
- package/tests/project_invite_suggestions.mjs +2 -2
- package/tests/promote_goal_id.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/rank_floor_wiring.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +1 -1
- package/tests/removal_proposal.mjs +2 -2
- package/tests/reward_gate.mjs +4 -4
- package/tests/reward_mode_cost_plus_only.mjs +1 -1
- package/tests/role_responsibilities.mjs +18 -12
- package/tests/role_stats_db.mjs +17 -16
- package/tests/scouting_rollup_fanout.mjs +5 -5
- package/tests/seed_tasks.mjs +5 -2
- package/tests/session_earnings_mirror_db.mjs +1 -1
- package/tests/session_reward_on_land_db.mjs +1 -1
- package/tests/session_reward_reconcile_db.mjs +2 -2
- package/tests/sessions_kit_pager.mjs +1 -1
- package/tests/settings_art_key_absent.mjs +2 -2
- package/tests/settings_main_role.mjs +21 -20
- package/tests/setup_flow.mjs +3 -3
- package/tests/setup_smoke.mjs +20 -0
- package/tests/ship_card.mjs +6 -6
- package/tests/specialities.mjs +16 -4
- package/tests/speciality_db_shape.mjs +6 -6
- package/tests/speciality_injection.mjs +17 -17
- package/tests/speciality_offer_ui.mjs +5 -5
- package/tests/speciality_offered.mjs +6 -5
- package/tests/speciality_session_skills.mjs +7 -7
- package/tests/speciality_skills.mjs +11 -11
- package/tests/speciality_walkthrough_ui.mjs +2 -2
- package/tests/sso_checkin_identity_spoof.mjs +2 -1
- package/tests/task_classifier_vocab.mjs +10 -0
- package/tests/task_cli.mjs +11 -3
- package/tests/task_criteria_unlink_db.mjs +2 -2
- package/tests/task_discipline_required.mjs +38 -16
- package/tests/task_goal_required.mjs +4 -4
- package/tests/task_needs_db.mjs +1 -1
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/update_pill.mjs +153 -2
- package/tests/upkeep_home.mjs +1 -1
- package/tests/v3_c1_options_proof.mjs +145 -0
- package/tests/visual_review.mjs +3 -3
- package/tests/watch_applications_queue.mjs +8 -5
- package/tests/watch_sealed_floor.mjs +3 -1
- package/tests/welcome_boot.mjs +12 -9
- package/tests/welcome_boot_browser.mjs +13 -13
- package/tests/worktree_helper.mjs +16 -1
package/tests/hall_nav.mjs
CHANGED
|
@@ -301,7 +301,8 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
301
301
|
// how your own Claude works for you, the personal half of "the city steers
|
|
302
302
|
// everyone, My agents steers yours". Its subject is the viewer's own agents.
|
|
303
303
|
// task 1005050: 'standing' (Accomplishments) left — your numbers are the one Profile page.
|
|
304
|
-
|
|
304
|
+
// task 1005144 (ADR 0401): 'my-agents' LEFT for the one options menu (the profile chip).
|
|
305
|
+
assert.deepEqual(nav[3].items.map((i) => i.id), ['profile'], 'You = your own numbers');
|
|
305
306
|
// task 1003444: 'people' LEFT the group — the owner deleted the hall's
|
|
306
307
|
// cross-project directory (ADR 0337 D5); its home is the hub, and /people
|
|
307
308
|
// redirects to /roster. Builders is the hall's directory now.
|
|
@@ -324,7 +325,7 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
324
325
|
// Economy and Diagrams explained what the Primer explains; they are its chapters
|
|
325
326
|
// now, so Docs is the Primer and the four references that are not a document.
|
|
326
327
|
assert.deepEqual(nav[6].items.map((i) => i.id),
|
|
327
|
-
['primer', 'commands', 'atlas', '
|
|
328
|
+
['primer', 'commands', 'atlas', 'apidocs'], 'Docs = reference material (Modules is an options menu entry since task 1005144)');
|
|
328
329
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
329
330
|
// 'project-settings' joins the Government group at the metic floor (task 1003280,
|
|
330
331
|
// goal 1000072) — it sits before 'government', which is the one archon-only item.
|
|
@@ -342,7 +343,12 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
342
343
|
// idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
|
|
343
344
|
// 'gate' (Autobongos) and 'sessions' LEFT the group in task 1005049 (owner decision
|
|
344
345
|
// 2026-11-03, idea 1001315): they are tabs on Watch, and /gate + /sessions redirect there.
|
|
345
|
-
|
|
346
|
+
// task 1005144 (ADR 0401): 'project-settings' and 'government' (Permissions) LEFT for the
|
|
347
|
+
// one options menu; their URLs open it (serve-internal.js RETIRED_TO_OPTIONS).
|
|
348
|
+
assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'money', 'deploy']);
|
|
349
|
+
for (const gone of ['/my-agents', '/project-settings', '/government', '/modules']) {
|
|
350
|
+
assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === gone), `no item points at ${gone}, an options menu entry now`);
|
|
351
|
+
}
|
|
346
352
|
assert.ok(!nav.flatMap((g) => g.items).some((i) => i.href === '/gate' || i.href === '/sessions'), 'no item points at a page that became a Watch tab');
|
|
347
353
|
// task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
|
|
348
354
|
// fails here instead of in front of the owner.
|
|
@@ -792,7 +798,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
|
|
|
792
798
|
// block (task 1004713). These drive the real shell and the real gov-positions.js.
|
|
793
799
|
|
|
794
800
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
795
|
-
const GOVERNING = ['city', 'board-room', 'watch', '
|
|
801
|
+
const GOVERNING = ['city', 'board-room', 'watch', 'money', 'deploy'];
|
|
796
802
|
const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
|
|
797
803
|
const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
|
|
798
804
|
|
|
@@ -821,7 +827,7 @@ test('POSITION: a Lead keeps every governing surface, and the seat names it', as
|
|
|
821
827
|
const b = await seated({ builder: { id: '424242', rank: 'thetes' }, positions: block([lead('planning', 'Planning Lead')]) });
|
|
822
828
|
assert.equal(b.shell.positions().title, 'Planning Lead');
|
|
823
829
|
// a thetes Lead: the seat opens the governing tabs their rank and permissions allow
|
|
824
|
-
for (const id of ['board-room', 'city', '
|
|
830
|
+
for (const id of ['board-room', 'city', 'profile']) assert.equal(b.navHidden(id), false, `${id} shows`);
|
|
825
831
|
});
|
|
826
832
|
|
|
827
833
|
test('POSITION: the fixture viewer holds what the fixture /me says', async () => {
|
|
@@ -833,10 +839,10 @@ test('POSITION: the fixture viewer holds what the fixture /me says', async () =>
|
|
|
833
839
|
|
|
834
840
|
test('POSITION: a builder with no seat sees no governing surface, and only those go', async () => {
|
|
835
841
|
// an archon the server says holds no seat: rank, craft and permissions do not make one
|
|
836
|
-
const b = await seated({ builder: { id: '424242', rank: 'archon', preferred_disciplines: ['
|
|
842
|
+
const b = await seated({ builder: { id: '424242', rank: 'archon', preferred_disciplines: ['artisan'] }, positions: block([]) });
|
|
837
843
|
assert.equal(b.shell.positions().has_position, false);
|
|
838
|
-
for (const id of ['board-room', 'city', 'watch', '
|
|
839
|
-
for (const id of ['home', 'work', 'goals', '
|
|
844
|
+
for (const id of ['board-room', 'city', 'watch', 'money']) assert.equal(b.navHidden(id), true, `${id} hides`);
|
|
845
|
+
for (const id of ['home', 'work', 'goals', 'profile', 'primer']) assert.equal(b.navHidden(id), false, `${id} still shows`);
|
|
840
846
|
});
|
|
841
847
|
|
|
842
848
|
test('POSITION: unknown is today\'s nav — a /me with no positions block hides nothing', async () => {
|
|
@@ -859,18 +865,19 @@ test('POSITION: ?as=nobody views the hall with no seat, and can only hide', asyn
|
|
|
859
865
|
const b = await seated({ builder: me.builder, positions: me.positions, search: '?as=nobody' });
|
|
860
866
|
assert.equal(b.shell.positions().as, 'nobody');
|
|
861
867
|
for (const id of GOVERNING) assert.equal(b.navHidden(id), true, `${id} hides for the stand-in`);
|
|
862
|
-
assert.equal(b.navHidden('
|
|
868
|
+
assert.equal(b.navHidden('profile'), false);
|
|
863
869
|
// a xenos stand-in stays a xenos: the pretend rank never rises above the real one
|
|
864
870
|
const x = await seated({ builder: { id: '9', rank: 'xenos' }, positions: block([lead('planning', 'Planning Lead')]), search: '?as=nobody' });
|
|
865
871
|
assert.equal(x.navHidden('goal-map'), true, 'a member page stays hidden from a real xenos');
|
|
866
872
|
});
|
|
867
873
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
assert.ok(
|
|
874
|
+
// task 1005144 (ADR 0401): My agents, Project settings, Permissions and Modules are entries
|
|
875
|
+
// in the one options menu, reached by the profile chip, which now says so.
|
|
876
|
+
test('the settings pages left the sidebar for the options menu, and the profile chip names it', () => {
|
|
877
|
+
const html = boot().sidebarHtml;
|
|
878
|
+
const ids = parseNav(html).flatMap((g) => g.items).map((i) => i.id);
|
|
879
|
+
for (const gone of ['my-agents', 'project-settings', 'government', 'modules']) assert.ok(!ids.includes(gone), `${gone} is not a nav item`);
|
|
880
|
+
assert.ok(html.includes('<a class="shell-user" id="shell-user" href="/settings" title="Options" aria-label="Options"'), 'the chip opens the menu and says so');
|
|
874
881
|
});
|
|
875
882
|
|
|
876
883
|
// task 1005050 (owner decision 2026-11-03, idea 1001315): Accomplishments
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// tests/hall_options_menu.mjs — the ONE options menu (task 1005144, BONGOS-V3 R04,
|
|
2
|
+
// goal 1000141, criterion game-opens-not-navigates; ADR 0401).
|
|
3
|
+
//
|
|
4
|
+
// Every setting and configuration is reached from one vertical, grouped menu with a
|
|
5
|
+
// search box at the top: the Settings page. The screens that were pages of their own
|
|
6
|
+
// (My agents, Project settings, Modules, Permissions, Market Watch's settings and rules)
|
|
7
|
+
// open INSIDE it, in a frame asking for the old address with ?embed=1, and a plain visit
|
|
8
|
+
// to an old address is sent to the menu with that screen open. These run the real
|
|
9
|
+
// options-menu.js (its pure half), the real shell-game.js in a DOM stub, and read the
|
|
10
|
+
// server's redirect map, so a menu entry, its old URL and its frame cannot drift apart.
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import vm from 'node:vm';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
21
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
22
|
+
const Menu = createRequire(import.meta.url)(path.join(HALL, 'options-menu.js'));
|
|
23
|
+
const SERVE = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
|
|
24
|
+
|
|
25
|
+
const ids = (menu) => menu.flatMap((g) => g.items.map((i) => i.id));
|
|
26
|
+
const signed = (rank) => ({ authed: true, rank });
|
|
27
|
+
|
|
28
|
+
test('the menu holds every settings surface: Settings\' own groups and the six screens that were pages', () => {
|
|
29
|
+
const all = Menu.entries();
|
|
30
|
+
const embeds = all.filter((e) => e.embed).map((e) => e.embed).sort();
|
|
31
|
+
assert.deepEqual(embeds, ['/government', '/market-watch', '/market-watch-rules', '/modules', '/my-agents', '/project-settings']);
|
|
32
|
+
for (const e of all.filter((x) => x.embed)) {
|
|
33
|
+
assert.ok(fs.existsSync(path.join(HALL, e.embed.slice(1) + '.html')), `${e.embed}.html exists for the frame`);
|
|
34
|
+
}
|
|
35
|
+
// every native entry is a real group on the Settings page, and every group has an entry
|
|
36
|
+
const html = read('settings.html');
|
|
37
|
+
const groups = new Set([...html.matchAll(/data-group="([a-z-]+)"/g)].map((m) => m[1]));
|
|
38
|
+
const natives = all.filter((e) => e.native).map((e) => e.id);
|
|
39
|
+
for (const id of natives) assert.ok(groups.has(id), `#${id} is a Settings group`);
|
|
40
|
+
for (const g of groups) assert.ok(natives.includes(g), `the ${g} group is reachable from the menu`);
|
|
41
|
+
// ids are unique, so one address opens one thing
|
|
42
|
+
assert.equal(new Set(all.map((e) => e.id)).size, all.length);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('the proof\'s three settings are all in the one menu: a sound, a CLI token and a module toggle', () => {
|
|
46
|
+
const menu = Menu.visibleMenu(signed('archon'));
|
|
47
|
+
const hit = (q) => Menu.search(q, menu)[0];
|
|
48
|
+
assert.equal(hit('sound').id, 'sound');
|
|
49
|
+
assert.deepEqual([hit('cli token').id, hit('cli token').anchor], ['access', 'cli']);
|
|
50
|
+
assert.equal(hit('module toggle').id, 'modules');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('who sees what: the same rank keys the sidebar used, and module and contribution gates', () => {
|
|
54
|
+
const xenos = ids(Menu.visibleMenu(signed('xenos')));
|
|
55
|
+
assert.ok(xenos.includes('account') && xenos.includes('my-agents') && xenos.includes('access') && xenos.includes('market-watch-rules'));
|
|
56
|
+
for (const id of ['modules', 'project-settings', 'permissions', 'market-watch']) assert.ok(!xenos.includes(id), `a xenos does not see ${id}`);
|
|
57
|
+
const thetes = ids(Menu.visibleMenu(signed('thetes')));
|
|
58
|
+
assert.ok(thetes.includes('modules') && !thetes.includes('project-settings'), 'Modules from member, as its sidebar item was');
|
|
59
|
+
const metic = ids(Menu.visibleMenu(signed('metic')));
|
|
60
|
+
assert.ok(metic.includes('project-settings') && metic.includes('permissions') && !metic.includes('market-watch'));
|
|
61
|
+
const archon = ids(Menu.visibleMenu(signed('archon')));
|
|
62
|
+
assert.ok(archon.includes('market-watch'), 'Market Watch settings: the owner, as its tab was');
|
|
63
|
+
assert.deepEqual(ids(Menu.visibleMenu({ authed: false, rank: 'archon' })), [], 'signed out: nothing');
|
|
64
|
+
const off = ids(Menu.visibleMenu(signed('archon'), { moduleOn: (k) => k !== 'market-watch' }));
|
|
65
|
+
assert.ok(!off.includes('market-watch') && !off.includes('market-watch-rules'), 'a module that is off has no entries');
|
|
66
|
+
assert.ok(!archon.includes('extensions'), 'Extensions only when a module contributed a panel');
|
|
67
|
+
assert.ok(ids(Menu.visibleMenu(signed('archon'), { contributed: true })).includes('extensions'));
|
|
68
|
+
// a group with nothing in it is not drawn
|
|
69
|
+
for (const g of Menu.visibleMenu(signed('xenos'))) assert.ok(g.items.length > 0, `${g.head} has entries`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the search: every word must match, label hits first, and a sub-place says where it is', () => {
|
|
73
|
+
const menu = Menu.visibleMenu(signed('archon'));
|
|
74
|
+
assert.deepEqual(Menu.search('', menu), []);
|
|
75
|
+
assert.deepEqual(Menu.search('zzzz', menu), []);
|
|
76
|
+
const talk = Menu.search('talks', menu)[0];
|
|
77
|
+
assert.deepEqual([talk.id, talk.anchor, talk.where], ['my-agents', 'talk', 'Your agents']);
|
|
78
|
+
assert.equal(Menu.search('PERMISSIONS', menu)[0].id, 'permissions', 'case does not matter');
|
|
79
|
+
// Enter opens the first line, so the first line must be the place meant
|
|
80
|
+
assert.equal(Menu.search('token', menu)[0].label, 'CLI access', 'the exact place above the group that holds it');
|
|
81
|
+
assert.equal(Menu.search('access', menu)[0].label, 'Access and keys', 'but a label hit on the group still leads');
|
|
82
|
+
assert.equal(Menu.search('danger zone', menu)[0].id, 'project-settings', 'a word only the entry\'s words carry still finds it');
|
|
83
|
+
// a viewer never finds what they cannot open
|
|
84
|
+
assert.ok(!Menu.search('permissions', Menu.visibleMenu(signed('xenos'))).some((h) => h.id === 'permissions'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('what an address opens: ?open=<entry> with its #anchor, a bare #<entry>, and nothing else', () => {
|
|
88
|
+
assert.deepEqual(Menu.target('?open=my-agents', '#talk'), { id: 'my-agents', anchor: 'talk' });
|
|
89
|
+
assert.deepEqual(Menu.target('?open=market-watch', ''), { id: 'market-watch', anchor: 'settings' }, 'an entry\'s own anchor');
|
|
90
|
+
assert.deepEqual(Menu.target('', '#sound'), { id: 'sound', anchor: null });
|
|
91
|
+
assert.equal(Menu.target('', '#cli'), null, 'a panel anchor is the page\'s own to land');
|
|
92
|
+
assert.equal(Menu.target('?open=nope', ''), null);
|
|
93
|
+
assert.equal(Menu.frameSrc(Menu.entry('my-agents'), 'talk', ''), '/my-agents?embed=1#talk');
|
|
94
|
+
assert.equal(Menu.frameSrc(Menu.entry('permissions'), null, '/builders'), '/builders/government?embed=1');
|
|
95
|
+
assert.equal(Menu.frameSrc(Menu.entry('sound'), null, ''), null, 'a native group has no frame');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('closing goes back only to a same-site path', () => {
|
|
99
|
+
assert.equal(Menu.closeTo('?from=' + encodeURIComponent('/goals#/goal/7')), '/goals#/goal/7');
|
|
100
|
+
for (const bad of ['//evil.example/x', 'https://evil.example', '/\\evil', '']) {
|
|
101
|
+
assert.equal(Menu.closeTo('?from=' + encodeURIComponent(bad)), null, `${bad || 'nothing'} is not a way back`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the server sends every old address to its entry, lets the frame through, and the two maps agree', () => {
|
|
106
|
+
const m = SERVE.match(/const RETIRED_TO_OPTIONS = Object\.freeze\((\{[\s\S]*?\})\);/);
|
|
107
|
+
assert.ok(m, 'the server map is a literal');
|
|
108
|
+
const server = vm.runInNewContext('(' + m[1] + ')');
|
|
109
|
+
const client = Object.fromEntries(Object.entries(Menu.RETIRED).map(([p, id]) => [p.slice(1), id]));
|
|
110
|
+
assert.deepEqual({ ...server }, client, 'serve-internal.js RETIRED_TO_OPTIONS and options-menu.js RETIRED are one map');
|
|
111
|
+
for (const id of Object.values(client)) assert.ok(Menu.entry(id), `${id} is a menu entry`);
|
|
112
|
+
const reDecl = 'const RETIRED_OPTIONS_RE = ';
|
|
113
|
+
const i = SERVE.indexOf(reDecl);
|
|
114
|
+
const rx = vm.runInNewContext(SERVE.slice(i + reDecl.length, SERVE.indexOf(';', i)));
|
|
115
|
+
for (const p of Object.keys(server)) {
|
|
116
|
+
for (const v of [`/builders/${p}`, `/builders/${p}/`, `/builders/${p}.html`]) assert.ok(rx.test(v), `${v} is redirected`);
|
|
117
|
+
}
|
|
118
|
+
for (const p of ['/builders/settings', '/builders/market-watch', '/builders/modulesx', '/modules/x/public/a.js']) assert.ok(!rx.test(p), `${p} is not`);
|
|
119
|
+
const block = SERVE.slice(i, SERVE.indexOf('});', i));
|
|
120
|
+
assert.match(block, /if \(embed === '1' && m\[1\] !== 'market-watch-settings'\) return next\(\);/, 'the frame passes through to the page and its own gate');
|
|
121
|
+
assert.match(block, /\/settings\?open=\$\{RETIRED_TO_OPTIONS\[m\[1\]\]\}/);
|
|
122
|
+
// it runs BEFORE the page gates, which still guard the framed request
|
|
123
|
+
assert.ok(i < SERVE.indexOf('PROJECT_SETTINGS_PAGE_RE.test(norm)'), 'ahead of the page-gate dispatch');
|
|
124
|
+
assert.match(SERVE, /GOVERNMENT_PAGE_RE\.test\(norm\)\) \{\s*return gdsAuth\.requireGovernmentPage/, 'the Permissions frame keeps its page gate');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('the Settings page IS the menu: one search over the groups, loaded before settings.js', () => {
|
|
128
|
+
const html = read('settings.html');
|
|
129
|
+
assert.match(html, /<nav class="opt-rail" id="settings-rail" aria-label="Options"><\/nav>/);
|
|
130
|
+
assert.ok(html.indexOf('options-menu.js') > 0 && html.indexOf('options-menu.js') < html.indexOf('/builders/settings.js'));
|
|
131
|
+
assert.match(html, /<title>Options — /);
|
|
132
|
+
const js = read('settings.js');
|
|
133
|
+
assert.match(js, /menu\.mount\(\{/, 'settings.js hands the menu the viewer');
|
|
134
|
+
assert.doesNotMatch(js, /makeSectionRail/, 'the old rail is gone, not drawn beside the menu');
|
|
135
|
+
const css = read('settings.css');
|
|
136
|
+
assert.match(css, /\.settings-groups > \.opt-embed\.opt-embed--on \{ display: block; \}/, 'one framed screen shows at a time');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---- shell-game.js: the embedded page and game mode's Escape --------------------
|
|
140
|
+
|
|
141
|
+
function bootGame({ search = '', framed = true, game = false, path: p = '/my-agents' } = {}) {
|
|
142
|
+
const docListeners = {};
|
|
143
|
+
const winListeners = {};
|
|
144
|
+
const timers = [];
|
|
145
|
+
const assigned = [];
|
|
146
|
+
const root = { dataset: {} };
|
|
147
|
+
const bodyAttrs = {};
|
|
148
|
+
const parent = { OTBOptions: { escapes: 0, escape() { this.escapes += 1; } } };
|
|
149
|
+
const win = {
|
|
150
|
+
location: { pathname: p, search, hash: '', origin: 'https://h', href: 'https://h' + p + search, assign(u) { assigned.push(u); } },
|
|
151
|
+
localStorage: { getItem: (k) => (k === 'hall-game' && game ? 'on' : null), setItem() {} },
|
|
152
|
+
sessionStorage: { getItem: () => null, setItem() {} },
|
|
153
|
+
addEventListener(t, fn) { (winListeners[t] ||= []).push(fn); },
|
|
154
|
+
setTimeout(fn) { timers.push(fn); },
|
|
155
|
+
};
|
|
156
|
+
win.parent = framed ? parent : win;
|
|
157
|
+
const document = {
|
|
158
|
+
documentElement: root,
|
|
159
|
+
body: { setAttribute(k, v) { bodyAttrs[k] = v; } },
|
|
160
|
+
addEventListener(t, fn) { (docListeners[t] ||= []).push(fn); },
|
|
161
|
+
};
|
|
162
|
+
const ctx = vm.createContext({ window: win, document, URLSearchParams, URL });
|
|
163
|
+
vm.runInContext(read('shell-game.js'), ctx);
|
|
164
|
+
return {
|
|
165
|
+
root, bodyAttrs, parent, assigned, game: win.OTBShellGame,
|
|
166
|
+
press(key, target = { tagName: 'DIV' }) {
|
|
167
|
+
const ev = { key, target, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
|
|
168
|
+
for (const fn of winListeners.keydown || []) fn(ev);
|
|
169
|
+
while (timers.length) timers.shift()();
|
|
170
|
+
return ev;
|
|
171
|
+
},
|
|
172
|
+
click(href, target = null) {
|
|
173
|
+
const attrs = { href };
|
|
174
|
+
if (target) attrs.target = target;
|
|
175
|
+
const a = { getAttribute: (k) => (k in attrs ? attrs[k] : null), setAttribute: (k, v) => { attrs[k] = v; } };
|
|
176
|
+
for (const fn of docListeners.click || []) fn({ target: { closest: () => a } });
|
|
177
|
+
return attrs.target || null;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
test('EMBEDDED: only a framed ?embed=1 page drops its chrome, and the switch is off', () => {
|
|
183
|
+
const framed = bootGame({ search: '?embed=1' });
|
|
184
|
+
assert.equal(framed.root.dataset.embed, 'on');
|
|
185
|
+
assert.equal(framed.bodyAttrs['data-fullscreen-switch'], 'off', 'no corner button, no Escape-to-leave');
|
|
186
|
+
assert.equal(framed.game.embedded(), true);
|
|
187
|
+
const alone = bootGame({ search: '?embed=1', framed: false });
|
|
188
|
+
assert.equal(alone.root.dataset.embed, undefined, 'the same URL opened on its own keeps its chrome');
|
|
189
|
+
assert.equal(bootGame({ search: '' }).root.dataset.embed, undefined);
|
|
190
|
+
const css = read('style.css');
|
|
191
|
+
assert.match(css, /html\[data-embed="on"\] \.app-sidebar,\s*html\[data-embed="on"\] \.app-scrim,\s*html\[data-embed="on"\] \.app-foot,\s*html\[data-embed="on"\] \.app-topbar \{ display: none; \}/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('EMBEDDED: an Escape nobody used goes to the menu; a link to another page leaves the frame', () => {
|
|
195
|
+
const p = bootGame({ search: '?embed=1' });
|
|
196
|
+
p.press('Escape');
|
|
197
|
+
assert.equal(p.parent.OTBOptions.escapes, 1, 'the menu got it');
|
|
198
|
+
p.press('Escape', { tagName: 'INPUT' });
|
|
199
|
+
assert.equal(p.parent.OTBOptions.escapes, 1, 'a field keeps its own Escape');
|
|
200
|
+
assert.equal(p.click('/task/42'), '_top', 'another page opens in the whole window');
|
|
201
|
+
assert.equal(p.click('#talk'), null, 'a place in the same page stays in the frame');
|
|
202
|
+
assert.equal(p.click('/goals', '_blank'), '_blank', 'a link that names its own target keeps it');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('GAME MODE: on the menu itself the last Escape closes it; outside game mode it is not the menu\'s', () => {
|
|
206
|
+
// the menu page registers window.OTBOptions; shell-game hands it the press
|
|
207
|
+
const menu = bootGame({ game: true, framed: false, path: '/settings' });
|
|
208
|
+
menu.parent.OTBOptions.escapes = 0;
|
|
209
|
+
const ev = menu.press('Escape');
|
|
210
|
+
assert.equal(ev.defaultPrevented, true, 'marked used, so the shell does not also bring the bars back');
|
|
211
|
+
assert.deepEqual(menu.assigned, ['/settings?from=%2Fsettings'], 'with no menu script loaded it opens the menu page');
|
|
212
|
+
const off = bootGame({ game: false, framed: false, path: '/goals' });
|
|
213
|
+
off.press('Escape');
|
|
214
|
+
assert.deepEqual(off.assigned, [], 'outside game mode the press stays the shell\'s');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('options-menu.js gives the menu its Escape: close in game mode, nothing outside it', () => {
|
|
218
|
+
const src = read('options-menu.js');
|
|
219
|
+
assert.match(src, /function escape\(\) \{\s*var game = window\.OTBShellGame;\s*if \(game && game\.on && game\.on\(\)\) close\(\);/);
|
|
220
|
+
assert.match(src, /root\.OTBOptions = \{/, 'the menu registers itself where shell-game.js looks');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// In game mode an Escape nobody used opens the options menu, so a pop-up that closes on
|
|
224
|
+
// Escape must say it used the press (ADR 0399 §2.4 row 5). These seven closed something and
|
|
225
|
+
// let the press fall through: before task 1005144 that also brought the bars back (the city
|
|
226
|
+
// card was task 1005188); after it, it would also have left the page for the menu.
|
|
227
|
+
test('every pop-up that closes on Escape spends the press, so the menu never opens from under it', () => {
|
|
228
|
+
const spent = {
|
|
229
|
+
'atlas.js': /ev\.key === 'Escape' && popup\.classList\.contains\('show'\)\) \{ ev\.preventDefault\(\); closePopup\(\);/,
|
|
230
|
+
'city.js': /if \(e\.key === 'Escape'\) \{ if \(e\.preventDefault\) e\.preventDefault\(\); closeCard\(\);/,
|
|
231
|
+
'home-gallery.js': /if \(isFs\(\) \|\| reading\.length\) e\.preventDefault\(\);/,
|
|
232
|
+
'task-detail.js': /e\.key === 'Escape' && document\.getElementById\('deeplink-card'\)\) \{ e\.preventDefault\(\); clearLink\(\);/,
|
|
233
|
+
'tweak-editor.js': /\$\('tw-openwrap'\)\.classList\.contains\('is-open'\)\) ev\.preventDefault\(\);/,
|
|
234
|
+
'update-pill.js': /state\.card && !state\.card\.hidden\) \{ e\.preventDefault\(\); closeCard\(\);/,
|
|
235
|
+
'diagram-viewer.js': /&& fullscreen\) \{ e\.preventDefault\(\); toggleFullscreen\(fullscreen\);/,
|
|
236
|
+
};
|
|
237
|
+
for (const [f, re] of Object.entries(spent)) assert.match(read(f), re, `${f} spends the Escape it used`);
|
|
238
|
+
// and a framed screen never asks for a craft: the menu page around it does
|
|
239
|
+
assert.match(read('craft-mode.js'), /if \(window\.OTBShellGame && window\.OTBShellGame\.embedded && window\.OTBShellGame\.embedded\(\)\) return;/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('the start screen\'s Options is the menu, and its first tip says what Escape does now', () => {
|
|
243
|
+
const src = read('shell-game.js');
|
|
244
|
+
assert.match(src, /\{ id: 'options', label: 'Options', href: '\/settings' \}/, 'Options opens the one options menu');
|
|
245
|
+
assert.match(src, /'Escape opens the options menu, where every setting lives\.'/);
|
|
246
|
+
assert.doesNotMatch(src, /Escape brings the navigation back/, 'no tip promises what Escape no longer does');
|
|
247
|
+
});
|
|
@@ -206,7 +206,7 @@ async function bootWatchLedger(overrides = {}) {
|
|
|
206
206
|
};
|
|
207
207
|
sandbox.globalThis = sandbox;
|
|
208
208
|
vm.createContext(sandbox);
|
|
209
|
-
for (const f of ['dom-utils.js', 'hall-kit.js', 'watch.js']) {
|
|
209
|
+
for (const f of ['dom-utils.js', 'hall-kit.js', 'watch-repo-invite.js', 'watch.js']) {
|
|
210
210
|
vm.runInContext(read(f), sandbox, { filename: f });
|
|
211
211
|
}
|
|
212
212
|
// load() is async: let every section's promise settle before reading the DOM.
|
package/tests/hall_palette.mjs
CHANGED
|
@@ -156,11 +156,10 @@ test('Government section anchors exist in government.js makeTabs', () => {
|
|
|
156
156
|
const ids = [...read('government.js').matchAll(/\{ id: '([a-z-]+)', label: '[^']*', panel:/g)].map((m) => m[1]);
|
|
157
157
|
assert.ok(ids.length === 2, `sanity: found the two government tabs (${ids})`);
|
|
158
158
|
assert.ok(!ids.includes('board-room'), 'the Board Room is a page, not a tab (ADR 0266)');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
159
|
+
// task 1005144: Permissions and the Constitution are an options menu entry, rank-gated
|
|
160
|
+
// in the menu, so the palette declares no /government row — one offered to every
|
|
161
|
+
// signed-in reader would land a sub-Metic one on a menu without that entry.
|
|
162
|
+
assert.equal(declaredHrefs((x) => x.startsWith('/government')).length, 0, 'no palette row points at the old page');
|
|
164
163
|
});
|
|
165
164
|
|
|
166
165
|
test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
|
|
@@ -182,8 +181,8 @@ test('My agents anchors are ones my-agents.js lands, and the moved Settings pane
|
|
|
182
181
|
const block = (src.match(/const ANCHORS = \{([\s\S]*?)\n \};/) || [, ''])[1];
|
|
183
182
|
const keys = [...block.matchAll(/^\s*([a-z-]+): \{/gm)].map((m) => m[1]);
|
|
184
183
|
assert.ok(keys.length >= 5, `sanity: my-agents.js ANCHORS (${keys})`);
|
|
185
|
-
const declared = declaredHrefs((x) => x.startsWith('/my-agents#'));
|
|
186
|
-
assert.ok(declared.length >= 5, 'the palette offers the agent settings where they live now');
|
|
184
|
+
const declared = declaredHrefs((x) => x.startsWith('/settings?open=my-agents#'));
|
|
185
|
+
assert.ok(declared.length >= 5, 'the palette offers the agent settings where they live now (the options menu, task 1005144)');
|
|
187
186
|
for (const h of declared) assert.ok(keys.includes(h.split('#')[1]), `${h} must be a my-agents.js anchor`);
|
|
188
187
|
assert.equal(declaredHrefs((x) => /\/settings#(interaction|wandering|rendering|voices|specialities)/.test(x)).length, 0,
|
|
189
188
|
'no palette row points at a Settings panel that left');
|
|
@@ -207,10 +206,10 @@ test('pages come from the sidebar, and a gated-off nav item is not a destination
|
|
|
207
206
|
test('section groups appear only when their owning nav item is visible', () => {
|
|
208
207
|
const all = bootPalette({ navItems: NAV_FIXTURE }).compute('');
|
|
209
208
|
assert.ok(all.some((r) => r.group === 'Profile'), 'Profile sections offered when nav-profile shows');
|
|
210
|
-
assert.ok(all.some((r) => r.group === '
|
|
209
|
+
assert.ok(all.some((r) => r.group === 'Options'), 'Options (the one menu, task 1005144) offered when the profile chip shows');
|
|
211
210
|
|
|
212
211
|
const gatedOut = bootPalette({ navItems: NAV_FIXTURE, visibleIds: [] }).compute('');
|
|
213
|
-
assert.ok(!gatedOut.some((r) => ['Profile', 'Tasks', '
|
|
212
|
+
assert.ok(!gatedOut.some((r) => ['Profile', 'Tasks', 'Options'].includes(r.group)),
|
|
214
213
|
'no sub-destinations when their owners are hidden');
|
|
215
214
|
});
|
|
216
215
|
|
|
@@ -164,7 +164,7 @@ function bootCharts() {
|
|
|
164
164
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
165
165
|
roleAxisLabel: () => 'Role',
|
|
166
166
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
167
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
167
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
168
168
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
169
169
|
ROLE_UNSET: 'unclassified',
|
|
170
170
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
@@ -308,7 +308,7 @@ function bootRender({ rows, status = 200, analytics = null, earnings = [] }) {
|
|
|
308
308
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
309
309
|
roleAxisLabel: () => 'Role',
|
|
310
310
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
311
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
311
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
312
312
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
313
313
|
ROLE_UNSET: 'unclassified',
|
|
314
314
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
@@ -328,7 +328,7 @@ function bootRender({ rows, status = 200, analytics = null, earnings = [] }) {
|
|
|
328
328
|
return { C: sandbox.window.OTBProfileCharts, api, requests, mount, controls };
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
const ship = (d, extra = {}) => ({ shipped_at: `${d}T10:00:00Z`, discipline: '
|
|
331
|
+
const ship = (d, extra = {}) => ({ shipped_at: `${d}T10:00:00Z`, discipline: 'artisan', credits_awarded: 10, credits_reward: 12, ...extra });
|
|
332
332
|
// an earnings row: what the ledger paid on one day, for one kind of credit
|
|
333
333
|
const pay = (d, credits, kind = 'session') => ({ day: d, kind, credits });
|
|
334
334
|
|
|
@@ -152,14 +152,16 @@ test('.btn and .btn--ghost are defined, and say what .btn-accent / .btn-ghost sa
|
|
|
152
152
|
assert.match(btn, /background:\s*var\(--accent\)/, '.btn is the accent door');
|
|
153
153
|
assert.match(btn, /color:\s*var\(--accent-on\)/, '--accent-on is the dark ink for the salmon');
|
|
154
154
|
const shape = ruleHolding(controlsCss, '.btn', 'border-radius');
|
|
155
|
-
|
|
155
|
+
// Helm (task 1005192, ADR 0402): the hall's corner is the cut; the pill is the fallback where the hall's tier is not loaded
|
|
156
|
+
assert.match(shape, /border-radius:\s*var\(--press-radius, var\(--radius-pill\)\)/, 'the cut corner in the hall, the pill elsewhere (DESIGN.md, The hall)');
|
|
157
|
+
assert.match(shape, /clip-path:\s*var\(--cut-path, none\)/, 'anything you can press takes the cut corner');
|
|
156
158
|
// collab.css's .btn--quiet sets only a border-COLOUR; a modifier cannot paint
|
|
157
159
|
// an edge that was never reserved, so the base has to carry a transparent one.
|
|
158
160
|
assert.match(shape, /border:\s*var\(--line\)\s+solid\s+transparent/,
|
|
159
161
|
'the edge is reserved so a modifier that sets only border-color still draws');
|
|
160
162
|
const ghost = ruleHolding(controlsCss, '.btn--ghost', 'background');
|
|
161
163
|
assert.ok(ghost, '.btn--ghost has a rule');
|
|
162
|
-
assert.match(ghost, /background:\s*transparent/, 'the quiet sibling is a hairline
|
|
164
|
+
assert.match(ghost, /background:\s*var\(--press-fill, transparent\)/, 'the quiet sibling is the press fill inside a hairline, never the accent');
|
|
163
165
|
assert.equal(ghost, ruleHolding(controlsCss, '.btn-ghost', 'background'), 'and .btn-ghost says the same, in the same rule');
|
|
164
166
|
assert.match(ghost, /border-color:\s*var\(--rule\)/, 'and it takes the reserved edge');
|
|
165
167
|
});
|
|
@@ -395,7 +397,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
395
397
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
396
398
|
roleAxisLabel: () => 'Role',
|
|
397
399
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
398
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
400
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
399
401
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
400
402
|
ROLE_UNSET: 'unclassified',
|
|
401
403
|
// task 1004038 — the kind axis asks the same question. Stubbed for the
|
|
@@ -446,7 +448,7 @@ const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
|
446
448
|
|
|
447
449
|
const BASE_TASK = {
|
|
448
450
|
id: '1003305', title: 'A record in the world', status: 'shipped', version_id: 'BONGOS-V1',
|
|
449
|
-
kind: 'feature', discipline: '
|
|
451
|
+
kind: 'feature', discipline: 'artisan', priority: 1, requires_rank: 'metic',
|
|
450
452
|
credits_reward: 60, est_minutes: 240, actual_minutes: 37,
|
|
451
453
|
created_at: '2026-08-28T00:00:00Z', description: 'the description', touches: ['a.js', 'b.js'],
|
|
452
454
|
dependencies: [], dependents: [],
|
|
@@ -222,7 +222,8 @@ test('every old link to those sections lands on My agents', () => {
|
|
|
222
222
|
// settings.js asks before it draws anything, and on every later hash
|
|
223
223
|
const html = read('settings.html');
|
|
224
224
|
assert.ok(html.indexOf('settings-moved.js') > 0 && html.indexOf('settings-moved.js') < html.indexOf('/builders/settings.js'), 'loaded before settings.js');
|
|
225
|
-
|
|
225
|
+
// task 1005144: My agents is an options menu entry now, so a moved link opens it in place.
|
|
226
|
+
assert.match(settingsJs, /OTBOptions\.activate\('my-agents', anchor\)/, 'opened in the menu, no page load');
|
|
226
227
|
assert.doesNotMatch(settingsJs, /'(interaction|wandering|rendering|voices)': '[a-z-]+-scroll'/, 'no PANEL_ANCHORS entry for a section that left');
|
|
227
228
|
});
|
|
228
229
|
|
package/tests/hall_sitemap.mjs
CHANGED
|
@@ -21,9 +21,10 @@ const model = gen.build();
|
|
|
21
21
|
test('the parse found the hall, so nothing below passes against an empty map', () => {
|
|
22
22
|
const files = fs.readdirSync(path.join(ROOT, 'modules/hall-ui/public')).filter((f) => f.endsWith('.html'));
|
|
23
23
|
assert.equal(model.pages.length, files.length, 'one row per page file');
|
|
24
|
-
//
|
|
25
|
-
assert.ok(model.nav.length >=
|
|
26
|
-
|
|
24
|
+
// 20 since task 1005144 moved four settings pages into the options menu.
|
|
25
|
+
assert.ok(model.nav.length >= 20, `expected the shell's nav, found ${model.nav.length} items`);
|
|
26
|
+
// task 1005141: three crafts — Artisan, Artist (Artistry + Ideating), Patron
|
|
27
|
+
for (const c of ['artisan', 'artist', 'patron']) {
|
|
27
28
|
assert.ok(model.crafts.tabs[c].length > model.crafts.always.length, `${c} lists tabs of its own`);
|
|
28
29
|
}
|
|
29
30
|
assert.ok(model.chrome.includes('shell.js') && model.chrome.includes('craft-mode.js'), 'the chrome is the shell');
|
|
@@ -87,10 +88,10 @@ test('the Primer links to Repo Atlas and Commands, and in the hall they open tho
|
|
|
87
88
|
assert.equal(gen.classify('/no-such-page', urls), false);
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
test('the street is
|
|
91
|
+
test('the street is Patron\'s home: no nav item, reached by clicking from the city', () => {
|
|
91
92
|
const street = model.pages.find((p) => p.url === '/street');
|
|
92
93
|
assert.ok(street, 'the street is a hall page');
|
|
93
94
|
assert.deepEqual(street.nav, []);
|
|
94
|
-
assert.ok(street.reachIn.includes('
|
|
95
|
+
assert.ok(street.reachIn.includes('patron'));
|
|
95
96
|
assert.ok(street.in.includes('/city'), 'the city links to the street');
|
|
96
97
|
});
|