@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
|
@@ -21,8 +21,8 @@ const conductor = require(path.join(ROOT, '.claude', 'hooks', 'conductor.js'));
|
|
|
21
21
|
|
|
22
22
|
const sp = (over = {}) => ({
|
|
23
23
|
inject: true,
|
|
24
|
-
contract: 'Speciality: Economics (under
|
|
25
|
-
discipline: '
|
|
24
|
+
contract: 'Speciality: Economics (under artist).',
|
|
25
|
+
discipline: 'artist',
|
|
26
26
|
name: 'Economics',
|
|
27
27
|
...over,
|
|
28
28
|
});
|
|
@@ -30,9 +30,9 @@ const claim = (discipline) => ({ task_id: '1', title: 't', discipline });
|
|
|
30
30
|
const ids = (out) => out.map((a) => a.id);
|
|
31
31
|
|
|
32
32
|
test('the contract is injected when its craft matches the claim in hand', () => {
|
|
33
|
-
const out = conductor.evaluateTriggers({ activeClaims: [claim('
|
|
34
|
-
assert.ok(ids(out).includes('speciality:
|
|
35
|
-
const entry = out.find((a) => a.id === 'speciality:
|
|
33
|
+
const out = conductor.evaluateTriggers({ activeClaims: [claim('artist')], specialities: [sp()] });
|
|
34
|
+
assert.ok(ids(out).includes('speciality:artist'), `expected the artist contract, got ${JSON.stringify(ids(out))}`);
|
|
35
|
+
const entry = out.find((a) => a.id === 'speciality:artist');
|
|
36
36
|
assert.match(entry.text, /Economics/);
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ test('a speciality for a DIFFERENT craft is not injected', () => {
|
|
|
41
41
|
// actively misleading at worst. This is the assertion that keeps the selection
|
|
42
42
|
// honest — without it, "inject everything active" would pass the test above.
|
|
43
43
|
const out = conductor.evaluateTriggers({
|
|
44
|
-
activeClaims: [claim('
|
|
44
|
+
activeClaims: [claim('artisan')],
|
|
45
45
|
specialities: [sp({ discipline: 'artist', contract: 'Speciality: Pixel art (under artist).' })],
|
|
46
46
|
});
|
|
47
47
|
assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 0);
|
|
@@ -55,7 +55,7 @@ test('with no active claim there is no craft, so nothing is injected', () => {
|
|
|
55
55
|
|
|
56
56
|
test('inject:false is honoured — an empty speciality spends no context', () => {
|
|
57
57
|
const out = conductor.evaluateTriggers({
|
|
58
|
-
activeClaims: [claim('
|
|
58
|
+
activeClaims: [claim('artist')],
|
|
59
59
|
specialities: [sp({ inject: false })],
|
|
60
60
|
});
|
|
61
61
|
assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 0);
|
|
@@ -63,24 +63,24 @@ test('inject:false is honoured — an empty speciality spends no context', () =>
|
|
|
63
63
|
|
|
64
64
|
test('one contract per craft, and two crafts in play get one each', () => {
|
|
65
65
|
const out = conductor.evaluateTriggers({
|
|
66
|
-
activeClaims: [claim('
|
|
67
|
-
specialities: [sp(), sp({ discipline: '
|
|
66
|
+
activeClaims: [claim('artist'), claim('artisan')],
|
|
67
|
+
specialities: [sp(), sp({ discipline: 'artisan', contract: 'Speciality: Compilers (under artisan).' })],
|
|
68
68
|
});
|
|
69
69
|
const got = ids(out).filter((i) => String(i).startsWith('speciality:'));
|
|
70
|
-
assert.deepEqual(got.sort(), ['speciality:
|
|
70
|
+
assert.deepEqual(got.sort(), ['speciality:artisan', 'speciality:artist']);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
test('malformed entries are skipped rather than thrown on', () => {
|
|
74
74
|
const out = conductor.evaluateTriggers({
|
|
75
|
-
activeClaims: [claim('
|
|
75
|
+
activeClaims: [claim('artist')],
|
|
76
76
|
specialities: [null, { inject: true }, sp({ contract: '' }), sp()],
|
|
77
77
|
});
|
|
78
78
|
assert.equal(ids(out).filter((i) => String(i).startsWith('speciality:')).length, 1);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
test('no specialities at all changes nothing about the hook', () => {
|
|
82
|
-
const before = conductor.evaluateTriggers({ activeClaims: [claim('
|
|
83
|
-
const after = conductor.evaluateTriggers({ activeClaims: [claim('
|
|
82
|
+
const before = conductor.evaluateTriggers({ activeClaims: [claim('artist')] });
|
|
83
|
+
const after = conductor.evaluateTriggers({ activeClaims: [claim('artist')], specialities: [] });
|
|
84
84
|
assert.deepEqual(ids(before), ids(after));
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -112,16 +112,16 @@ test('the adoption read selects the contract itself', () => {
|
|
|
112
112
|
test('two claims in two worktrees: each session gets only its own contract', () => {
|
|
113
113
|
const claims = [
|
|
114
114
|
{ ...claim('artist'), task_id: '1', worktree_name: 'task-1' },
|
|
115
|
-
{ ...claim('
|
|
115
|
+
{ ...claim('artisan'), task_id: '2', worktree_name: 'task-2' },
|
|
116
116
|
];
|
|
117
117
|
const specs = [
|
|
118
118
|
sp({ discipline: 'artist', contract: 'Speciality: Pixel art (under artist).' }),
|
|
119
|
-
sp({ discipline: '
|
|
119
|
+
sp({ discipline: 'artisan', contract: 'Speciality: Compilers (under artisan).' }),
|
|
120
120
|
];
|
|
121
121
|
const got = (worktree) => ids(conductor.evaluateTriggers({ activeClaims: claims, specialities: specs, worktree }))
|
|
122
122
|
.filter((i) => String(i).startsWith('speciality:'));
|
|
123
123
|
assert.deepEqual(got('task-1'), ['speciality:artist']);
|
|
124
|
-
assert.deepEqual(got('task-2'), ['speciality:
|
|
124
|
+
assert.deepEqual(got('task-2'), ['speciality:artisan']);
|
|
125
125
|
// No claim bound to this tree (main checkout): today's behaviour, every craft in play.
|
|
126
|
-
assert.deepEqual(got('main-checkout').sort(), ['speciality:
|
|
126
|
+
assert.deepEqual(got('main-checkout').sort(), ['speciality:artisan', 'speciality:artist']);
|
|
127
127
|
});
|
|
@@ -34,8 +34,8 @@ const INSTALLED = [
|
|
|
34
34
|
{ name: 'status', does: 'Shows how far along a version is.', cost: 'Free, and it changes nothing.' },
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: '
|
|
38
|
-
const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: '
|
|
37
|
+
const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: 'artisan', summary: '', contract_md: '', visibility: 'private', adoptable: true, mine: true, skills: ['recall', 'retired-skill'], ...over });
|
|
38
|
+
const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: 'artist', summary: '', contract_md: '', visibility: 'public', adoptable: true, mine: false, skills: ['recall'], ...over });
|
|
39
39
|
|
|
40
40
|
// The ticked boxes in rendered markup.
|
|
41
41
|
const checkedIn = (html) => [...html.matchAll(/<input type="checkbox" name="skill" value="([^"]+)" checked/g)].map((m) => m[1]);
|
|
@@ -74,7 +74,7 @@ for (const page of SPECIALITY_PAGES) {
|
|
|
74
74
|
const h = await boot(page);
|
|
75
75
|
const form = h.el('speciality-create');
|
|
76
76
|
assert.deepEqual(Object.keys(form.elements).sort(), ['contract_md', 'discipline', 'name', 'sellable', 'summary', 'visibility']);
|
|
77
|
-
assert.deepEqual(form.elements.discipline.querySelectorAll('option').map((o) => o.attrs.value), ['', '
|
|
77
|
+
assert.deepEqual(form.elements.discipline.querySelectorAll('option').map((o) => o.attrs.value), ['', 'artisan', 'artist', 'ui']);
|
|
78
78
|
assert.deepEqual(form.elements.visibility.querySelectorAll('option').map((o) => o.attrs.value), ['private', 'project', 'public']);
|
|
79
79
|
assert.ok(form.querySelector('details#speciality-create-offer'), 'the picker is inside the form');
|
|
80
80
|
assert.ok(form.querySelector('#speciality-create-skills'), 'with the host its choices fill');
|
|
@@ -191,12 +191,12 @@ for (const page of SPECIALITY_PAGES) {
|
|
|
191
191
|
assert.deepEqual(checkedIn(host.innerHTML), [], 'a new speciality starts offering nothing');
|
|
192
192
|
const form = h.el('speciality-create');
|
|
193
193
|
form.elements.name.value = 'New one';
|
|
194
|
-
form.elements.discipline.value = '
|
|
194
|
+
form.elements.discipline.value = 'artisan';
|
|
195
195
|
h.tick('#speciality-create-skills input[value="status"]');
|
|
196
196
|
await h.submit('#speciality-create');
|
|
197
197
|
const post = writes(h).find((r) => r.method === 'POST' && r.path === '/specialities');
|
|
198
198
|
assert.ok(post, 'created');
|
|
199
|
-
assert.deepEqual(post.body, { name: 'New one', discipline: '
|
|
199
|
+
assert.deepEqual(post.body, { name: 'New one', discipline: 'artisan', summary: '', contract_md: '', visibility: 'private', sellable: false, skills: ['status'] });
|
|
200
200
|
assert.ok(h.toasts.includes('Speciality created.'));
|
|
201
201
|
});
|
|
202
202
|
|
|
@@ -25,7 +25,7 @@ const db = require(path.join(ROOT, 'modules', 'specialities', 'db.js'));
|
|
|
25
25
|
|
|
26
26
|
const ME = '1000025';
|
|
27
27
|
const spec = (over = {}) => ({
|
|
28
|
-
id: '1', name: 'Economics', discipline: '
|
|
28
|
+
id: '1', name: 'Economics', discipline: 'artist',
|
|
29
29
|
owner_kind: 'builder', owner_builder_id: '1000099',
|
|
30
30
|
visibility: 'public', offered: true, ...over,
|
|
31
31
|
});
|
|
@@ -55,13 +55,13 @@ test('a builder-owned speciality set to "project" is not suggested to others', (
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test('a speciality you already hold is not suggested back to you', () => {
|
|
58
|
-
const adoptions = [{ id: '1', discipline: '
|
|
58
|
+
const adoptions = [{ id: '1', discipline: 'artist', active: true }];
|
|
59
59
|
assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test('an inactive past adoption still suppresses the suggestion', () => {
|
|
63
63
|
// You have had it; re-suggesting something you dropped is nagging, not helping.
|
|
64
|
-
const adoptions = [{ id: '1', discipline: '
|
|
64
|
+
const adoptions = [{ id: '1', discipline: 'artist', active: false }];
|
|
65
65
|
assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -69,12 +69,13 @@ test('a craft you already have an ACTIVE speciality in yields no suggestion', ()
|
|
|
69
69
|
// One active speciality per craft is a DB constraint (the partial unique index),
|
|
70
70
|
// so offering a second in the same craft is offering something that would be
|
|
71
71
|
// refused. Surfacing it as neither is the honest answer.
|
|
72
|
-
const adoptions = [{ id: '77', discipline: '
|
|
72
|
+
const adoptions = [{ id: '77', discipline: 'artist', active: true }];
|
|
73
73
|
assert.deepEqual(S.offeredTo([spec()], viewer, adoptions), []);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test('a different craft is still suggested while one craft is taken', () => {
|
|
77
|
-
|
|
77
|
+
// Since task 1005141 ideator folded into artist, so the "other" craft here is artisan.
|
|
78
|
+
const adoptions = [{ id: '77', discipline: 'artisan', active: true }];
|
|
78
79
|
assert.deepEqual(S.offeredTo([spec()], viewer, adoptions).map((s) => s.id), ['1']);
|
|
79
80
|
});
|
|
80
81
|
|
|
@@ -29,7 +29,7 @@ const { test, summary } = makeRunner();
|
|
|
29
29
|
const LEAN = /chose to lean on these skills/;
|
|
30
30
|
|
|
31
31
|
// ---- the pure line ----------------------------------------------------------
|
|
32
|
-
const row = (over = {}) => ({ id: 9, slug: 'economics', name: 'Economics', discipline: '
|
|
32
|
+
const row = (over = {}) => ({ id: 9, slug: 'economics', name: 'Economics', discipline: 'artist', summary: '', contract_md: '', skills: ['recall', 'status'], enabled_skills: [], active: true, ...over });
|
|
33
33
|
|
|
34
34
|
await test('enabled skills are named in the contract', () => {
|
|
35
35
|
const d = S.describeForApi(row({ enabled_skills: ['recall', 'status'] }));
|
|
@@ -100,7 +100,7 @@ await test('the port is the one the router registered', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
await test('an adoption with enabled skills reaches the session in its craft', async () => {
|
|
103
|
-
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('
|
|
103
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artist')]);
|
|
104
104
|
const sp = specEntries(out);
|
|
105
105
|
assert.equal(sp.length, 1);
|
|
106
106
|
assert.match(sp[0].text, /Economics/);
|
|
@@ -108,19 +108,19 @@ await test('an adoption with enabled skills reaches the session in its craft', a
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
await test('with nothing enabled the session gets the contract and not a word more', async () => {
|
|
111
|
-
const out = await sessionFor([row({ enabled_skills: [] })], [claim('
|
|
111
|
+
const out = await sessionFor([row({ enabled_skills: [] })], [claim('artist')]);
|
|
112
112
|
const sp = specEntries(out);
|
|
113
113
|
assert.equal(sp.length, 1, 'the speciality itself is still there');
|
|
114
114
|
assert.doesNotMatch(sp[0].text, LEAN);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
await test('craft-scoped like the contract: an engineering session hears nothing of an ideator\'s skills', async () => {
|
|
118
|
-
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('
|
|
118
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artisan')]);
|
|
119
119
|
assert.equal(specEntries(out).length, 0);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
await test('an inactive adoption relays nothing, skills included', async () => {
|
|
123
|
-
const out = await sessionFor([row({ active: false, enabled_skills: ['recall'] })], [claim('
|
|
123
|
+
const out = await sessionFor([row({ active: false, enabled_skills: ['recall'] })], [claim('artist')]);
|
|
124
124
|
assert.equal(specEntries(out).length, 0);
|
|
125
125
|
});
|
|
126
126
|
|
|
@@ -128,7 +128,7 @@ await test('a failed skills read degrades to the offered filter, never a lost co
|
|
|
128
128
|
const saved = skills.installedSkillsCached;
|
|
129
129
|
skills.installedSkillsCached = () => { throw new Error('disk gone'); };
|
|
130
130
|
try {
|
|
131
|
-
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('
|
|
131
|
+
const out = await sessionFor([row({ enabled_skills: ['recall'] })], [claim('artist')]);
|
|
132
132
|
assert.match(specEntries(out)[0].text, /\/recall/);
|
|
133
133
|
} finally { skills.installedSkillsCached = saved; }
|
|
134
134
|
});
|
|
@@ -165,7 +165,7 @@ await test('three active adoptions cost ONE training read, and each keeps its ow
|
|
|
165
165
|
try {
|
|
166
166
|
store.adoptions = [
|
|
167
167
|
row({ id: 9, name: 'Economics', contract_md: 'Mine.' }),
|
|
168
|
-
row({ id: 10, name: 'Law', discipline: '
|
|
168
|
+
row({ id: 10, name: 'Law', discipline: 'artisan', contract_md: 'Theirs.' }),
|
|
169
169
|
row({ id: 11, name: 'Music', discipline: 'artist', contract_md: 'Gone.' }),
|
|
170
170
|
row({ id: 12, name: 'Old', discipline: 'ui', active: false }),
|
|
171
171
|
];
|
|
@@ -147,16 +147,16 @@ await test('an unreadable root contributes nothing rather than throwing', () =>
|
|
|
147
147
|
|
|
148
148
|
// ---- 2. the pure rules ------------------------------------------------------
|
|
149
149
|
const INSTALLED = new Set(['recall', 'status', 'costing']);
|
|
150
|
-
const draft = (over = {}) => ({ name: 'Economics', discipline: '
|
|
150
|
+
const draft = (over = {}) => ({ name: 'Economics', discipline: 'artist', owner_kind: 'builder', owner_builder_id: 1, ...over });
|
|
151
151
|
const codes = (r) => r.errors.map((e) => e.code);
|
|
152
152
|
|
|
153
153
|
await test('a speciality may offer skills this instance has', () => {
|
|
154
|
-
const r = S.validateDraft(draft({ skills: ['recall', 'costing'] }), { disciplines: ['
|
|
154
|
+
const r = S.validateDraft(draft({ skills: ['recall', 'costing'] }), { disciplines: ['artist'], installedSkills: INSTALLED });
|
|
155
155
|
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
await test('a skill this instance does NOT have is refused, by name', () => {
|
|
159
|
-
const r = S.validateDraft(draft({ skills: ['recall', 'teleport'] }), { disciplines: ['
|
|
159
|
+
const r = S.validateDraft(draft({ skills: ['recall', 'teleport'] }), { disciplines: ['artist'], installedSkills: INSTALLED });
|
|
160
160
|
assert.equal(r.ok, false);
|
|
161
161
|
const e = r.errors.find((x) => x.code === 'unknown_skill');
|
|
162
162
|
assert.ok(e, JSON.stringify(r.errors));
|
|
@@ -164,14 +164,14 @@ await test('a skill this instance does NOT have is refused, by name', () => {
|
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
await test('a non-empty offered set nothing could check is refused, never admitted', () => {
|
|
167
|
-
const r = S.validateDraft(draft({ skills: ['recall'] }), { disciplines: ['
|
|
167
|
+
const r = S.validateDraft(draft({ skills: ['recall'] }), { disciplines: ['artist'] });
|
|
168
168
|
assert.ok(codes(r).includes('uncheckable'));
|
|
169
169
|
// and an empty set needs no check
|
|
170
|
-
assert.equal(S.validateDraft(draft({ skills: [] }), { disciplines: ['
|
|
170
|
+
assert.equal(S.validateDraft(draft({ skills: [] }), { disciplines: ['artist'] }).ok, true);
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
await test('shape: not a list, a bad name, a duplicate, too many', () => {
|
|
174
|
-
const v = (skillsList) => codes(S.validateDraft(draft({ skills: skillsList }), { disciplines: ['
|
|
174
|
+
const v = (skillsList) => codes(S.validateDraft(draft({ skills: skillsList }), { disciplines: ['artist'], installedSkills: INSTALLED }));
|
|
175
175
|
assert.ok(v('recall').includes('invalid'));
|
|
176
176
|
assert.ok(v(['Recall']).includes('invalid_name'));
|
|
177
177
|
assert.ok(v(['recall', 'recall']).includes('duplicate'));
|
|
@@ -201,7 +201,7 @@ await test('the migration stores an EMPTY enabled set by default, not a UI defau
|
|
|
201
201
|
await test('adopting never writes the enabled set, so every new adoption takes the empty default', async () => {
|
|
202
202
|
const seen = [];
|
|
203
203
|
const exec = async (text, params) => { seen.push({ text, params }); return { rows: [{ builder_id: 1, speciality_id: 9, enabled_skills: [] }], rowCount: 1 }; };
|
|
204
|
-
const row = await db.adopt({ builderId: 1, specialityId: 9, discipline: '
|
|
204
|
+
const row = await db.adopt({ builderId: 1, specialityId: 9, discipline: 'artist' }, exec);
|
|
205
205
|
const insert = seen.find((q) => /INSERT INTO specialities_adoptions/.test(q.text));
|
|
206
206
|
assert.ok(insert, 'the adopt INSERT ran');
|
|
207
207
|
assert.ok(!/enabled_skills\s*[,)]/.test(insert.text.split('VALUES')[0]), 'the INSERT column list does not name enabled_skills');
|
|
@@ -220,7 +220,7 @@ await test('the enabled set is written only on the caller\'s own ACTIVE adoption
|
|
|
220
220
|
|
|
221
221
|
// ---- 4. the real router -----------------------------------------------------
|
|
222
222
|
const store = {
|
|
223
|
-
speciality: { id: 9, name: 'Economics', discipline: '
|
|
223
|
+
speciality: { id: 9, name: 'Economics', discipline: 'artist', owner_kind: 'builder', owner_builder_id: 1, visibility: 'public', sellable: false, skills: ['recall', 'status'] },
|
|
224
224
|
adoption: null, created: null, updated: null,
|
|
225
225
|
};
|
|
226
226
|
db.getSpeciality = async () => ({ ...store.speciality });
|
|
@@ -235,7 +235,7 @@ skills.installedSkillsCached = () => new Map([
|
|
|
235
235
|
]);
|
|
236
236
|
db.listAdoptions = async () => (store.adoption ? [{ id: 9, active: store.adoption.active, enabled_skills: store.adoption.enabled_skills }] : []);
|
|
237
237
|
api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'metic' }; next(); };
|
|
238
|
-
api.enabledDisciplines = () => ['
|
|
238
|
+
api.enabledDisciplines = () => ['artist', 'artisan', 'artist'];
|
|
239
239
|
|
|
240
240
|
const express = require('express');
|
|
241
241
|
const app = express();
|
|
@@ -252,14 +252,14 @@ const call = async (method, url, body) => {
|
|
|
252
252
|
|
|
253
253
|
try {
|
|
254
254
|
await test('POST /specialities stores an offered set of installed skills', async () => {
|
|
255
|
-
const r = await call('POST', '/specialities', { name: 'Economics', discipline: '
|
|
255
|
+
const r = await call('POST', '/specialities', { name: 'Economics', discipline: 'artist', skills: ['recall', 'costing'] });
|
|
256
256
|
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
257
257
|
assert.deepEqual(store.created.skills, ['recall', 'costing']);
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
await test('POST /specialities refuses a skill the instance does not have', async () => {
|
|
261
261
|
store.created = null;
|
|
262
|
-
const r = await call('POST', '/specialities', { name: 'Econ2', discipline: '
|
|
262
|
+
const r = await call('POST', '/specialities', { name: 'Econ2', discipline: 'artist', skills: ['teleport'] });
|
|
263
263
|
assert.equal(r.status, 400);
|
|
264
264
|
assert.ok(JSON.stringify(r.body).includes('unknown_skill'));
|
|
265
265
|
assert.equal(store.created, null, 'nothing was written');
|
|
@@ -39,14 +39,14 @@ const explained = (name, extra = {}) => ({
|
|
|
39
39
|
|
|
40
40
|
function speciality(over = {}) {
|
|
41
41
|
return {
|
|
42
|
-
id: 9, name: 'Economics', discipline: '
|
|
42
|
+
id: 9, name: 'Economics', discipline: 'artist', summary: 'Markets.', contract_md: '',
|
|
43
43
|
visibility: 'public', adoptable: true, mine: false, skills: ['recall', 'status', 'goal-review', 'gone'],
|
|
44
44
|
...over,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const SKILLS_BODY = {
|
|
49
|
-
speciality: { id: 9, name: 'Economics', discipline: '
|
|
49
|
+
speciality: { id: 9, name: 'Economics', discipline: 'artist' },
|
|
50
50
|
adopted: true,
|
|
51
51
|
enabled: [],
|
|
52
52
|
skills: [
|
|
@@ -83,6 +83,7 @@ require.cache[moduleApiPath] = {
|
|
|
83
83
|
resolveEnv: () => null, // no hub signing key ⇒ device legs 503
|
|
84
84
|
userAgent: () => 'cloud-bongos-tests',
|
|
85
85
|
LIVE_RANK_LADDER: ['xenos', 'thetes', 'metic', 'archon'],
|
|
86
|
+
crafts: require('../src/crafts.js'), // the rollup normalizes reported disciplines (task 1005141)
|
|
86
87
|
},
|
|
87
88
|
};
|
|
88
89
|
|
|
@@ -163,7 +164,7 @@ const rollupBody = (over = {}) => ({
|
|
|
163
164
|
karma: 999999,
|
|
164
165
|
tasks_shipped: 9999,
|
|
165
166
|
rank: 'archon',
|
|
166
|
-
disciplines: ['
|
|
167
|
+
disciplines: ['artisan'],
|
|
167
168
|
...over,
|
|
168
169
|
});
|
|
169
170
|
|
|
@@ -93,3 +93,13 @@ test('no hall page carries its own copy of the kind list', () => {
|
|
|
93
93
|
`${page} spells out the kind vocabulary — fill it from /me task_kinds via OTB.fillKindFilter instead`);
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
// task 1005141 folded the ideator into the artist craft. The art cues and the
|
|
98
|
+
// ideas cues are now ONE craft's score, so a task with one of each is an artist
|
|
99
|
+
// task — scored as two lanes it read as a tie and fell to hand-triage.
|
|
100
|
+
test('an even art/ideas task is the artist craft, not a tie', () => {
|
|
101
|
+
const { classifyTaskDiscipline } = classifier;
|
|
102
|
+
assert.equal(classifyTaskDiscipline('Sprites for the quest', '', []).discipline, 'artist');
|
|
103
|
+
// ui still competes as its own lane.
|
|
104
|
+
assert.equal(classifyTaskDiscipline('UI redesign wireframes for the landing page', '', []).discipline, 'ui');
|
|
105
|
+
});
|
package/tests/task_cli.mjs
CHANGED
|
@@ -24,13 +24,13 @@ t('requires --title and --description', () => {
|
|
|
24
24
|
assert.ok(errors.some((e) => /--description/.test(e)));
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
t('minimal valid: defaults kind=feature, discipline=
|
|
27
|
+
t('minimal valid: defaults kind=feature, discipline=artisan, status=backlog', () => {
|
|
28
28
|
const { body, errors } = buildTaskBody(['--title', 'T', '--description', 'D']);
|
|
29
29
|
assert.equal(errors.length, 0);
|
|
30
30
|
assert.equal(body.title, 'T');
|
|
31
31
|
assert.equal(body.description, 'D');
|
|
32
32
|
assert.equal(body.kind, 'feature');
|
|
33
|
-
assert.equal(body.discipline, '
|
|
33
|
+
assert.equal(body.discipline, 'artisan');
|
|
34
34
|
assert.equal(body.status, 'backlog');
|
|
35
35
|
assert.equal(body.source, 'bongos-task-cli');
|
|
36
36
|
// optional fields absent unless provided
|
|
@@ -92,7 +92,15 @@ t('hallLink builds a hall URL from the API base origin, falls back cleanly', ()
|
|
|
92
92
|
|
|
93
93
|
t('enum vocab is the expected shape', () => {
|
|
94
94
|
assert.ok(VALID_KINDS.includes('feature') && VALID_KINDS.includes('bug'));
|
|
95
|
-
|
|
95
|
+
// task 1005141: engineer is 'artisan'; ideator folded into 'artist'.
|
|
96
|
+
assert.deepEqual(VALID_DISCIPLINES, ['artisan', 'artist', 'ui']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t('the OLD --discipline spellings still work, mapped to the new craft (task 1005141)', () => {
|
|
100
|
+
assert.equal(buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'engineer']).body.discipline, 'artisan');
|
|
101
|
+
assert.equal(buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'ideator']).body.discipline, 'artist');
|
|
102
|
+
const { errors } = buildTaskBody(['--title', 'T', '--description', 'D', '--discipline', 'wizard']);
|
|
103
|
+
assert.ok(errors.some((e) => /--discipline/.test(e)), 'an unknown craft is still refused');
|
|
96
104
|
});
|
|
97
105
|
|
|
98
106
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
@@ -70,8 +70,8 @@ try {
|
|
|
70
70
|
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'unlink-s','unlink-signer','Unlink Signer','archon')`, [SIGNER]);
|
|
71
71
|
|
|
72
72
|
const goal = await db.createGoal({ versionId: VER, title: 'Unlink goal', createdBy: SIGNER });
|
|
73
|
-
const shipped = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The shipped half', kind: 'feature', discipline: '
|
|
74
|
-
const open = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The open half', kind: 'feature', discipline: '
|
|
73
|
+
const shipped = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The shipped half', kind: 'feature', discipline: 'artisan', creditsReward: 10 });
|
|
74
|
+
const open = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The open half', kind: 'feature', discipline: 'artisan', creditsReward: 10 });
|
|
75
75
|
await q(`UPDATE tasks SET status='shipped', shipped_at = now() - interval '1 hour' WHERE id=$1`, [shipped.id]);
|
|
76
76
|
|
|
77
77
|
// Two criteria, the same two links on each. Only the first carries a sign-off.
|
|
@@ -18,13 +18,16 @@ const { resolveWriteDiscipline, WRITABLE_DISCIPLINES, VALID_DISCIPLINES } = clas
|
|
|
18
18
|
|
|
19
19
|
// --- the writable set -------------------------------------------------------
|
|
20
20
|
|
|
21
|
-
test('the writable set is the
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
assert.deepEqual([...WRITABLE_DISCIPLINES].sort(), ['
|
|
21
|
+
test('the writable set is the crafts — ui included, unclassified excluded', () => {
|
|
22
|
+
// ADR 0272 (2026-09-09) keeps `ui` as a module-contributed discipline and the owner
|
|
23
|
+
// reconfirmed it on 2026-09-17 when abandoning task 1002987; dropping it would
|
|
24
|
+
// break live ui rows. Task 1005141 (BONGOS-V3) renamed engineer → 'artisan' and
|
|
25
|
+
// folded ideator into 'artist', so the four became three: artisan, artist, ui.
|
|
26
|
+
assert.deepEqual([...WRITABLE_DISCIPLINES].sort(), ['artisan', 'artist', 'ui']);
|
|
27
27
|
assert.ok(!WRITABLE_DISCIPLINES.has('unclassified'), 'unclassified is a triage state, not a craft');
|
|
28
|
+
for (const old of ['engineer', 'ideator']) {
|
|
29
|
+
assert.ok(!WRITABLE_DISCIPLINES.has(old), `'${old}' is an old spelling, not a writable craft`);
|
|
30
|
+
}
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
test('the READ set still carries unclassified, so old rows and filters resolve', () => {
|
|
@@ -41,9 +44,20 @@ test('an explicit valid discipline is honoured verbatim', () => {
|
|
|
41
44
|
}
|
|
42
45
|
});
|
|
43
46
|
|
|
47
|
+
test('an OLD spelling is normalized to the new craft, not refused (task 1005141)', () => {
|
|
48
|
+
assert.deepEqual(resolveWriteDiscipline({ discipline: 'engineer', title: 'x' }), { discipline: 'artisan' });
|
|
49
|
+
assert.deepEqual(resolveWriteDiscipline({ discipline: 'ideator', title: 'x' }), { discipline: 'artist' });
|
|
50
|
+
assert.equal(classifier.normalizeDisciplineValue('engineer'), 'artisan');
|
|
51
|
+
assert.equal(classifier.normalizeDisciplineValue('ideator'), 'artist');
|
|
52
|
+
assert.equal(classifier.normalizeDisciplineValue('ui'), 'ui');
|
|
53
|
+
assert.equal(classifier.normalizeDisciplineValue(null), null, 'a non-string passes through for the caller to judge');
|
|
54
|
+
assert.deepEqual(resolveWriteDiscipline({ discipline: 'wizard', title: 'x' }), { error: 'bad_discipline' },
|
|
55
|
+
'normalizing does not turn an unknown word into a craft');
|
|
56
|
+
});
|
|
57
|
+
|
|
44
58
|
test('an omitted discipline is INFERRED, and reports that it was', () => {
|
|
45
59
|
const r = resolveWriteDiscipline({ title: 'Refactor the database migration script' });
|
|
46
|
-
assert.equal(r.discipline, '
|
|
60
|
+
assert.equal(r.discipline, 'artisan');
|
|
47
61
|
assert.equal(r.inferred, true, 'the caller must be able to tell a guess from a statement');
|
|
48
62
|
assert.ok(r.confidence >= 0.5);
|
|
49
63
|
});
|
|
@@ -52,7 +66,7 @@ test('an explicit "unclassified" is treated as no answer at all, not as a value'
|
|
|
52
66
|
// modules/ideas/inbox.js sent this literal string on every promotion. If it were
|
|
53
67
|
// honoured as a value, the create path would still mint the rows this task removes.
|
|
54
68
|
const r = resolveWriteDiscipline({ discipline: 'unclassified', title: 'Refactor the database migration script' });
|
|
55
|
-
assert.equal(r.discipline, '
|
|
69
|
+
assert.equal(r.discipline, 'artisan');
|
|
56
70
|
assert.equal(r.inferred, true);
|
|
57
71
|
});
|
|
58
72
|
|
|
@@ -109,13 +123,14 @@ test('createTask no longer has an "unclassified" default to fall back on', () =>
|
|
|
109
123
|
|
|
110
124
|
// --- the migration: the absence is the thing under test ---------------------
|
|
111
125
|
|
|
112
|
-
test('core_247
|
|
126
|
+
test('core_247 narrowed the CHECK to the four crafts of its day and REJECTED unclassified', () => {
|
|
127
|
+
// core_247 is history: it predates task 1005141, so it names engineer/ideator.
|
|
128
|
+
// The live constraint is core_296's — see the next test.
|
|
113
129
|
const sql = fs.readFileSync(path.join(ROOT, 'migrations', 'core_247_discipline_required.sql'), 'utf8');
|
|
114
130
|
const m = sql.match(/CHECK \(discipline IN \(([^)]*)\)\)/);
|
|
115
131
|
assert.ok(m, 'the migration must re-add a discipline CHECK constraint');
|
|
116
132
|
const accepted = m[1].split(',').map((s) => s.trim().replace(/'/g, '')).sort();
|
|
117
|
-
assert.deepEqual(accepted, ['artist', 'engineer', 'ideator', 'ui']
|
|
118
|
-
'the accepted set must match WRITABLE_DISCIPLINES exactly');
|
|
133
|
+
assert.deepEqual(accepted, ['artist', 'engineer', 'ideator', 'ui']);
|
|
119
134
|
assert.ok(!accepted.includes('unclassified'), 'the whole point: the constraint rejects it');
|
|
120
135
|
// The net that makes the migration safe on a checkout where nobody ran the script.
|
|
121
136
|
assert.match(sql, /UPDATE tasks SET discipline = 'engineer'[\s\S]*?WHERE discipline = 'unclassified'/,
|
|
@@ -125,10 +140,17 @@ test('core_247 narrows the CHECK to the four crafts and REJECTS unclassified', (
|
|
|
125
140
|
test('the migration constraint and the code agree on the accepted set', () => {
|
|
126
141
|
// Two places state the vocabulary; a drift between them is a 500 at write time
|
|
127
142
|
// (code accepts a value the DB rejects) or dead validation (the reverse).
|
|
128
|
-
|
|
143
|
+
// The live CHECK is core_296's (task 1005141). It is a deliberate SUPERSET: it
|
|
144
|
+
// still admits the old 'engineer'/'ideator' because `bongos upgrade` rolls code
|
|
145
|
+
// back without rolling the DB back. So every writable craft is accepted, and the
|
|
146
|
+
// only extras are exactly those two old spellings — never 'unclassified'.
|
|
147
|
+
const sql = fs.readFileSync(path.join(ROOT, 'migrations', 'core_296_crafts_artisan_artist.sql'), 'utf8');
|
|
129
148
|
const accepted = sql.match(/CHECK \(discipline IN \(([^)]*)\)\)/)[1]
|
|
130
149
|
.split(',').map((s) => s.trim().replace(/'/g, '')).sort();
|
|
131
|
-
assert.
|
|
150
|
+
for (const d of WRITABLE_DISCIPLINES) assert.ok(accepted.includes(d), `the DB must accept writable '${d}'`);
|
|
151
|
+
const extras = accepted.filter((d) => !WRITABLE_DISCIPLINES.has(d)).sort();
|
|
152
|
+
assert.deepEqual(extras, ['engineer', 'ideator'], 'only the rollback spellings may be extra');
|
|
153
|
+
assert.ok(!accepted.includes('unclassified'));
|
|
132
154
|
});
|
|
133
155
|
|
|
134
156
|
// --- the create vectors that must keep working ------------------------------
|
|
@@ -139,7 +161,7 @@ test('every direct createTask call site names a discipline or resolves one', ()
|
|
|
139
161
|
['modules/ideas/inbox.js', /discipline: \(lifecycle\.resolveWriteDiscipline/],
|
|
140
162
|
['modules/copy-desk/routes/copy-desk.js', /discipline: 'artist'/],
|
|
141
163
|
['modules/onboarding/newcomer-restock.js', /^\s*discipline,\s*$/m],
|
|
142
|
-
['modules/lifecycle/kickoff-seed.js', /discipline: t\.discipline \|\| '
|
|
164
|
+
['modules/lifecycle/kickoff-seed.js', /discipline: t\.discipline \|\| 'artisan'/],
|
|
143
165
|
['modules/lifecycle/cascade.js', /discipline: rule\.discipline/],
|
|
144
166
|
['scripts/bongos/triage.js', /discipline: resolveWriteDiscipline\(/],
|
|
145
167
|
];
|
|
@@ -150,7 +172,7 @@ test('every direct createTask call site names a discipline or resolves one', ()
|
|
|
150
172
|
});
|
|
151
173
|
|
|
152
174
|
test('init.js no longer clobbers its own discipline default with undefined', () => {
|
|
153
|
-
// The spread ran AFTER `t.discipline || '
|
|
175
|
+
// The spread ran AFTER `t.discipline || 'artisan'`, so `discipline: undefined` in
|
|
154
176
|
// the extras object reset it to nothing — which is why a freshly scaffolded
|
|
155
177
|
// instance was born with role-less starter tasks.
|
|
156
178
|
// Comment lines are stripped first: the fix's own explanation names the old
|
|
@@ -159,5 +181,5 @@ test('init.js no longer clobbers its own discipline default with undefined', ()
|
|
|
159
181
|
const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'init.js'), 'utf8')
|
|
160
182
|
.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');
|
|
161
183
|
assert.ok(!/discipline: undefined/.test(src), 'a later spread must not undo the default');
|
|
162
|
-
assert.match(src, /discipline: t\.discipline \|\| '
|
|
184
|
+
assert.match(src, /discipline: t\.discipline \|\| 'artisan'/);
|
|
163
185
|
});
|
|
@@ -47,13 +47,13 @@ const permissivePool = {
|
|
|
47
47
|
|
|
48
48
|
test('a create with no goalId THROWS instead of quietly filing into a bucket', async () => {
|
|
49
49
|
await assert.rejects(
|
|
50
|
-
() => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: '
|
|
50
|
+
() => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan' }, { pool: permissivePool }),
|
|
51
51
|
/goalId is required/,
|
|
52
52
|
'the old behaviour was to succeed and say nothing');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test('the error tells the caller both ways out', async () => {
|
|
56
|
-
const err = await createTask({ versionId: 'BONGOS-V1', title: 't', discipline: '
|
|
56
|
+
const err = await createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan' }, { pool: permissivePool })
|
|
57
57
|
.then(() => null, (e) => e);
|
|
58
58
|
assert.ok(err, 'must reject');
|
|
59
59
|
assert.match(err.message, /BV1\.R11/, 'names the rule, so the reason is findable');
|
|
@@ -63,7 +63,7 @@ test('the error tells the caller both ways out', async () => {
|
|
|
63
63
|
test('only a literal true opts in — a truthy value still throws', async () => {
|
|
64
64
|
for (const v of [1, 'yes', {}, 'true']) {
|
|
65
65
|
await assert.rejects(
|
|
66
|
-
() => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: '
|
|
66
|
+
() => createTask({ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan', allowCatchAll: v }, { pool: permissivePool }),
|
|
67
67
|
/goalId is required/,
|
|
68
68
|
`${JSON.stringify(v)} must not open the hatch`);
|
|
69
69
|
}
|
|
@@ -73,7 +73,7 @@ test('naming a goal needs no flag at all', async () => {
|
|
|
73
73
|
// The ordinary path must stay ordinary: a caller with a goal never learns this
|
|
74
74
|
// parameter exists.
|
|
75
75
|
await createTask(
|
|
76
|
-
{ versionId: 'BONGOS-V1', title: 't', discipline: '
|
|
76
|
+
{ versionId: 'BONGOS-V1', title: 't', discipline: 'artisan', goalId: 77 },
|
|
77
77
|
{ pool: permissivePool, client: permissivePool },
|
|
78
78
|
);
|
|
79
79
|
});
|
package/tests/task_needs_db.mjs
CHANGED
|
@@ -58,7 +58,7 @@ try {
|
|
|
58
58
|
[id, `tn-${id}`, `tn-${id}`, grants]);
|
|
59
59
|
}
|
|
60
60
|
for (const [id, needs] of [[T_NONE, '{}'], [T_OWNER, '{owner}'], [T_SERVER, '{server}']]) {
|
|
61
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,needs) VALUES ($1,$2,$3,'ready',10,'feature','
|
|
61
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,needs) VALUES ($1,$2,$3,'ready',10,'feature','artisan',$4::text[])`,
|
|
62
62
|
[id, VER, `tn ${id}`, needs]);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -38,7 +38,7 @@ const api = require('../src/module-api.js');
|
|
|
38
38
|
// used: a hall-ui module ship that changed a page file.
|
|
39
39
|
const SHIP = {
|
|
40
40
|
id: '555', title: 'Reword the studio greeting', status: 'shipped',
|
|
41
|
-
discipline: '
|
|
41
|
+
discipline: 'artisan', kind: 'feature', source: 'manual', module_key: 'hall-ui',
|
|
42
42
|
version_id: 'BONGOS-V2', goal_id: '1000095',
|
|
43
43
|
touches: ['modules/hall-ui/public/studio.html'],
|
|
44
44
|
shipped_at: new Date('2026-09-22T09:00:00Z'),
|