@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
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// criterion wa6-roles-read-true-in-public).
|
|
4
4
|
//
|
|
5
5
|
// The done-when: "engineer, artist and ideator fixtures each render their own
|
|
6
|
-
// headline, matching the mock".
|
|
6
|
+
// headline, matching the mock". Since task 1005141 (BONGOS-V3) there are two
|
|
7
|
+
// crafts: the engineer is the ARTISAN, and the ideator folded into the ARTIST,
|
|
8
|
+
// so the ideas-only fixture is an artist whose pages count is zero. Three
|
|
9
|
+
// layers, each run for real:
|
|
7
10
|
//
|
|
8
11
|
// 1. The words. profile-roles.js (pure) over the three harness fixtures the
|
|
9
12
|
// renders were taken from: each craft's tally, tiles and subtitle.
|
|
@@ -32,9 +35,16 @@ const OTBKit = require('../modules/hall-ui/public/hall-kit.js');
|
|
|
32
35
|
const ROLES_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile-roles.js'), 'utf8');
|
|
33
36
|
const PAGE_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.js'), 'utf8');
|
|
34
37
|
|
|
35
|
-
const
|
|
38
|
+
const ARTISAN = FIX('builders__profile');
|
|
36
39
|
const ARTIST = FIX('builders__2001__profile');
|
|
37
|
-
|
|
40
|
+
// The ideas-only artist, as the server sends it: role-stats.js merges the page
|
|
41
|
+
// half in, and with copy-desk present a person with no pages gets true zeros
|
|
42
|
+
// (never a missing field), and `credits` is the craft's ledger share.
|
|
43
|
+
const IDEAS_FIX = FIX('builders__2002__profile');
|
|
44
|
+
const IDEAS_ARTIST = { ...IDEAS_FIX, roles: { ...IDEAS_FIX.roles, artist: {
|
|
45
|
+
...IDEAS_FIX.roles.artist,
|
|
46
|
+
pages_approved: 0, pages: 0, surfaces_touched: 0, credits: 186, this_week: 0, surfaces: [], unavailable: false,
|
|
47
|
+
} } };
|
|
38
48
|
const OPTS = { fmt: (n) => Number(n).toLocaleString('en-US'), currency: 'credits' };
|
|
39
49
|
const words = (items) => items.map((t) => `${t.n} ${t.w}`);
|
|
40
50
|
|
|
@@ -42,63 +52,71 @@ const { test, summary } = makeRunner();
|
|
|
42
52
|
|
|
43
53
|
// ---- 1. the words --------------------------------------------------------
|
|
44
54
|
|
|
45
|
-
await test('an
|
|
46
|
-
const v = headline(
|
|
55
|
+
await test('an artisan leads with works shipped, and keeps karma', () => {
|
|
56
|
+
const v = headline(ARTISAN, OPTS);
|
|
47
57
|
assert.deepEqual(words(v.tally), ['4,630 credits', '24 works shipped', '1 karma']);
|
|
48
58
|
assert.deepEqual(words(v.tiles).slice(0, 4), ['24 works shipped', '4,630 credits', '1 karma', '2 achievements']);
|
|
49
59
|
assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '24 works shipped', 'the main craft leads the tally');
|
|
50
60
|
assert.deepEqual(v.surfaces, [], 'the per-surface rows are the artist\'s');
|
|
51
61
|
});
|
|
52
62
|
|
|
53
|
-
await test('an artist who never shipped a task leads with pages approved and the per-surface share', () => {
|
|
63
|
+
await test('an artist who never shipped a task leads with pages approved, her ideas and the per-surface share', () => {
|
|
54
64
|
const v = headline(ARTIST, OPTS);
|
|
55
|
-
assert.deepEqual(words(v.tally), ['412 credits', '9 pages approved', '
|
|
56
|
-
|
|
65
|
+
assert.deepEqual(words(v.tally), ['412 credits', '9 pages approved', '4 thoughts filed'],
|
|
66
|
+
'one number from each half of the craft: pages, then thoughts (task 1005141)');
|
|
67
|
+
assert.deepEqual(words(v.tiles), ['9 pages approved', '+38 this week', '270 credits', '4 thoughts filed', '1 ignited',
|
|
68
|
+
'0 Full Ideas', '42 total size', '85% still alive', '6 karma'], 'the pages tiles, then the ideas tiles, in ONE set');
|
|
69
|
+
assert.equal(v.tiles[2].f, 'pages and ideas', 'the craft credits are both halves');
|
|
57
70
|
assert.equal(v.tiles[0].f, '3 surfaces');
|
|
58
71
|
assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '9 pages approved', 'the main craft leads the tally');
|
|
59
72
|
assert.ok(!v.tally.some((t) => t.w === 'works shipped'), 'an artist\'s tally never says works shipped');
|
|
60
73
|
assert.ok(!('sub' in v), 'no sub-line is computed: the page has none (task 1004827)');
|
|
61
74
|
assert.deepEqual(v.surfaces.map((s) => s.line), ['6 of 12 tweaked', '2 of 19 tweaked', '1 of 6 tweaked']);
|
|
62
|
-
assert.ok(!words(v.tiles).some((w) => /works shipped/.test(w)), 'no
|
|
63
|
-
assert.equal(words(v.tiles).at(-1), '6 karma', 'karma ends every non-
|
|
75
|
+
assert.ok(!words(v.tiles).some((w) => /works shipped/.test(w)), 'no artisan figure on an artist\'s numbers');
|
|
76
|
+
assert.equal(words(v.tiles).at(-1), '6 karma', 'karma ends every non-artisan set (the owner, 2026-10-01)');
|
|
64
77
|
});
|
|
65
78
|
|
|
66
|
-
await test('an
|
|
67
|
-
const v = headline(
|
|
68
|
-
assert.deepEqual(words(v.tally), ['186 credits', '14 thoughts filed', '7 ignited']);
|
|
69
|
-
assert.deepEqual(words(v.tiles), ['
|
|
70
|
-
|
|
71
|
-
assert.
|
|
72
|
-
assert.
|
|
79
|
+
await test('an artist who only files ideas shows thoughts, size and what is still alive', () => {
|
|
80
|
+
const v = headline(IDEAS_ARTIST, OPTS);
|
|
81
|
+
assert.deepEqual(words(v.tally), ['186 credits', '14 thoughts filed', '7 ignited'], 'no approved page: the lead is the ideas, never a zero (task 1005141)');
|
|
82
|
+
assert.deepEqual(words(v.tiles), ['0 pages approved', '+0 this week', '186 credits', '14 thoughts filed', '7 ignited',
|
|
83
|
+
'3 Full Ideas', '186~ total size', '72% still alive', '3 karma']);
|
|
84
|
+
assert.equal(v.tiles[5].f, '1 ratified by the board');
|
|
85
|
+
assert.match(v.tiles[6].title, /Includes 24 credits of historical estimate/, 'the ~ says what it means');
|
|
86
|
+
assert.deepEqual(v.surfaces, [], 'no surface rows without pages');
|
|
73
87
|
});
|
|
74
88
|
|
|
75
89
|
await test('several crafts each show, main first, in the server\'s order', () => {
|
|
76
|
-
assert.deepEqual(headline(ARTIST, OPTS).crafts.map((c) => c.label), ['Artist, main role', '
|
|
77
|
-
assert.deepEqual(headline(
|
|
78
|
-
// The other craft adds its one headline tile after the main craft's set
|
|
79
|
-
|
|
80
|
-
const
|
|
90
|
+
assert.deepEqual(headline(ARTIST, OPTS).crafts.map((c) => c.label), ['Artist, main role'], 'one artist chip, never two');
|
|
91
|
+
assert.deepEqual(headline(ARTISAN, OPTS).crafts.map((c) => c.label), ['Artisan, main role', 'Artist']);
|
|
92
|
+
// The other craft adds its one headline tile after the main craft's set; the
|
|
93
|
+
// artist's names their pages, with their thoughts beneath.
|
|
94
|
+
const second = headline(ARTISAN, OPTS).tiles.at(-1);
|
|
95
|
+
assert.equal(second.w, 'pages approved');
|
|
96
|
+
assert.equal(second.f, '9 thoughts filed');
|
|
97
|
+
const reordered = { ...ARTIST, roles: { ...ARTIST.roles, main: 'artisan', shown: ['artisan', 'artist'], artisan: { shipped: 0, credits: 0 } } };
|
|
81
98
|
const v = headline(reordered, OPTS);
|
|
82
|
-
assert.deepEqual(v.crafts.map((c) => c.label), ['
|
|
83
|
-
assert.equal(words(v.tiles).at(-
|
|
99
|
+
assert.deepEqual(v.crafts.map((c) => c.label), ['Artisan, main role', 'Artist']);
|
|
100
|
+
assert.equal(words(v.tiles).at(-1), '9 pages approved', 'the other craft\'s tile closes the artisan set');
|
|
101
|
+
assert.equal(v.tiles.at(-1).f, '4 thoughts filed');
|
|
84
102
|
assert.deepEqual(v.surfaces, [], 'the per-surface rows follow the MAIN craft, not any craft');
|
|
85
103
|
});
|
|
86
104
|
|
|
87
105
|
await test('a number the instance could not read is a dash, never a zero', () => {
|
|
88
|
-
const noDesk = { ...ARTIST, roles: { ...ARTIST.roles, artist: { pages_approved: null,
|
|
106
|
+
const noDesk = { ...ARTIST, roles: { ...ARTIST.roles, artist: { ...ARTIST.roles.artist, pages_approved: null, pages: null, this_week: null, surfaces_touched: null, surfaces: [] } } };
|
|
89
107
|
const v = headline(noDesk, OPTS);
|
|
90
|
-
assert.deepEqual(words(v.tally).slice(1), ['— pages approved', '
|
|
108
|
+
assert.deepEqual(words(v.tally).slice(1), ['— pages approved', '4 thoughts filed'], 'the page half unknown, the ideas half still read');
|
|
91
109
|
assert.equal(v.tiles[0].f, '', 'no "0 surfaces" under an unknown count');
|
|
92
|
-
const noWork = { ...
|
|
110
|
+
const noWork = { ...IDEAS_ARTIST, roles: { ...IDEAS_ARTIST.roles, artist: { ...IDEAS_ARTIST.roles.artist, alive: { share: null, has_work: false, attributable: null } } } };
|
|
93
111
|
assert.deepEqual(headline(noWork, OPTS).tiles.at(-2), { n: '—', w: 'still alive', f: 'nothing has come of them yet' });
|
|
94
|
-
const untraced = { ...
|
|
112
|
+
const untraced = { ...IDEAS_ARTIST, roles: { ...IDEAS_ARTIST.roles, artist: { ...IDEAS_ARTIST.roles.artist, alive: { share: null, has_work: true, attributable: false } } } };
|
|
95
113
|
assert.equal(headline(untraced, OPTS).tiles.at(-2).f, 'not measurable yet');
|
|
96
114
|
});
|
|
97
115
|
|
|
98
|
-
await test('a response from before the roles block reads as the engineer it always was', () => {
|
|
99
|
-
const { roles, ...old } =
|
|
116
|
+
await test('a response from before the roles block reads as the artisan (the engineer) it always was', () => {
|
|
117
|
+
const { roles, ...old } = ARTISAN;
|
|
100
118
|
const v = headline(old, OPTS);
|
|
101
|
-
assert.deepEqual(v.crafts.map((c) => c.label), ['
|
|
119
|
+
assert.deepEqual(v.crafts.map((c) => c.label), ['Artisan, main role']);
|
|
102
120
|
assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '24 works shipped', 'the main craft leads the tally');
|
|
103
121
|
});
|
|
104
122
|
|
|
@@ -124,7 +142,7 @@ async function boot(profileData) {
|
|
|
124
142
|
};
|
|
125
143
|
const windowObj = {
|
|
126
144
|
__BRANDING__: { currency: { label: 'credits' } },
|
|
127
|
-
__MODULES__: { responsibilities: {
|
|
145
|
+
__MODULES__: { responsibilities: { artisan: 'E.', artist: 'A.' } },
|
|
128
146
|
BongosClient: { createClient: () => api },
|
|
129
147
|
// The real kit draws the surface bars; the section rail needs a real DOM and
|
|
130
148
|
// is not what this suite is about, so it is left out (profile.js skips it).
|
|
@@ -154,22 +172,23 @@ const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
|
154
172
|
await test('the real page draws the artist\'s tally, chips, tiles and surface rows', async () => {
|
|
155
173
|
const p = await boot(ARTIST);
|
|
156
174
|
assert.equal(p.sub, '', 'no sub-line: the head names the person, rank and headline (task 1004827)');
|
|
157
|
-
assert.match(text(p.head), /Artist, main role
|
|
158
|
-
assert.
|
|
175
|
+
assert.match(text(p.head), /Artist, main role Answerable for/, 'one artist chip, and no ideator chip beside it');
|
|
176
|
+
assert.ok(!/Ideator/.test(p.head), 'the ideator is not a craft any more (task 1005141)');
|
|
177
|
+
assert.match(text(p.head), /412 credits 9 pages approved 4 thoughts filed/);
|
|
159
178
|
assert.match(p.head, /Answerable for:/, 'the responsibility line stays');
|
|
160
|
-
assert.match(text(p.nums), /^9 pages approved 3 surfaces 270 credits
|
|
179
|
+
assert.match(text(p.nums), /^9 pages approved 3 surfaces \+38 this week 270 credits pages and ideas 4 thoughts filed 1 ignited/);
|
|
161
180
|
assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3, 'one kit bar row per surface');
|
|
162
181
|
assert.match(text(p.nums), /Landing 6 of 12 tweaked/);
|
|
163
182
|
assert.ok(!/works shipped/i.test(p.sub + p.head + p.nums), 'nothing on an artist\'s header says works shipped');
|
|
164
183
|
});
|
|
165
184
|
|
|
166
|
-
await test('the real page draws the
|
|
167
|
-
const i = await boot(
|
|
185
|
+
await test('the real page draws the ideas-only artist\'s and the artisan\'s own headlines', async () => {
|
|
186
|
+
const i = await boot(IDEAS_ARTIST);
|
|
168
187
|
assert.match(text(i.head), /186 credits 14 thoughts filed 7 ignited/);
|
|
169
188
|
assert.match(text(i.nums), /186~ total size/);
|
|
170
189
|
assert.match(text(i.nums), /72% still alive/);
|
|
171
|
-
assert.ok(!i.nums.includes('kit-bars'), 'no surface rows for an
|
|
172
|
-
const e = await boot({ ...
|
|
190
|
+
assert.ok(!i.nums.includes('kit-bars'), 'no surface rows for an artist with no pages');
|
|
191
|
+
const e = await boot({ ...ARTISAN, recent_ships: [] }); // the works ledger is not this suite's
|
|
173
192
|
assert.equal(e.sub, '', 'no sub-line (task 1004827)');
|
|
174
193
|
assert.match(text(e.head), /4,630 credits 24 works shipped 1 karma/);
|
|
175
194
|
});
|
|
@@ -181,21 +200,29 @@ await test('the server\'s roles block, through JSON, renders the artist\'s headl
|
|
|
181
200
|
const { readRoleStats } = require('../src/bongos/role-stats.js');
|
|
182
201
|
seams._reset();
|
|
183
202
|
seams.registerProvider('reward', {
|
|
184
|
-
craftForPreference: (d) => ({
|
|
185
|
-
async creditsByCraft(ids) { return new Map([[String(ids[0]), {
|
|
203
|
+
craftForPreference: (d) => ({ artisan: 'artisan', artist: 'artist', ui: 'artist' }[d] || null),
|
|
204
|
+
async creditsByCraft(ids) { return new Map([[String(ids[0]), { artisan: 0, artist: 412, other: 0, total: 412 }]]); },
|
|
186
205
|
});
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
// Each port answers ITS half of the artist block, as in production.
|
|
207
|
+
const PAGE_KEYS = ['pages_approved', 'pages', 'surfaces_touched', 'credits', 'this_week', 'week_since', 'surfaces'];
|
|
208
|
+
const pick = (keep) => Object.fromEntries(Object.entries(ARTIST.roles.artist).filter(([k]) => keep(k)));
|
|
209
|
+
seams.registerProvider('copy-desk.artistStats', async () => pick((k) => PAGE_KEYS.includes(k)));
|
|
210
|
+
seams.registerProvider('ideas.ideatorStats', async () => pick((k) => !PAGE_KEYS.includes(k)));
|
|
189
211
|
const roles = await readRoleStats('2001', {
|
|
190
212
|
shippedCount: 0,
|
|
191
|
-
getRoleChoice: async () => ({ main_discipline: 'artist', preferred_disciplines: ['artist'
|
|
213
|
+
getRoleChoice: async () => ({ main_discipline: 'artist', preferred_disciplines: ['artist'] }),
|
|
192
214
|
});
|
|
215
|
+
assert.equal(roles.ideator, undefined, 'no ideator block on the wire');
|
|
216
|
+
assert.equal(roles.artist.filed, 4, 'the ideas half reached the artist block');
|
|
217
|
+
assert.equal(roles.artist.pages_approved, 9, 'and so did the pages half');
|
|
218
|
+
assert.equal(roles.artist.credits, 412, 'the craft credits are the ledger split, both halves');
|
|
193
219
|
seams._reset();
|
|
194
220
|
const { roles: _fixture, ...rest } = ARTIST;
|
|
195
221
|
const p = await boot({ ...rest, roles });
|
|
196
222
|
assert.equal(p.sub, '', 'no sub-line (task 1004827)');
|
|
197
|
-
assert.match(text(p.head), /Artist, main role
|
|
198
|
-
assert.match(text(p.head), /412 credits 9 pages approved
|
|
223
|
+
assert.match(text(p.head), /Artist, main role Answerable for/);
|
|
224
|
+
assert.match(text(p.head), /412 credits 9 pages approved 4 thoughts filed/);
|
|
225
|
+
assert.match(text(p.nums), /412 credits pages and ideas/);
|
|
199
226
|
assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3);
|
|
200
227
|
});
|
|
201
228
|
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// over the REAL page inventory, and economy's REAL fold of the ledger rows:
|
|
11
11
|
//
|
|
12
12
|
// Ines Varga an artist who has never shipped a task: pages and ideas only
|
|
13
|
-
// Tobi Rahn an
|
|
14
|
-
//
|
|
13
|
+
// Tobi Rahn an artist (the old ideator, folded in by task 1005141) who has
|
|
14
|
+
// never shipped a task: thoughts only, no pages
|
|
15
|
+
// Mara Okonkwo an artisan, who also earned a little from an idea
|
|
15
16
|
//
|
|
16
17
|
// The SQL halves (which ledger rows the classification and the per-person
|
|
17
18
|
// earnings reach) run against a real Postgres in tests/role_stats_db.mjs; here
|
|
@@ -40,29 +41,32 @@ async function t(name, fn) {
|
|
|
40
41
|
|
|
41
42
|
const INES = 201; const TOBI = 202; const MARA = 203; const NEW = 204; const UI = 205; const LEA = 206;
|
|
42
43
|
const ROWS = {
|
|
43
|
-
[INES]: { id: INES, github_login: 'ines', display_name: 'Ines Varga', total_credits: 312, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'
|
|
44
|
-
[TOBI]: { id: TOBI, github_login: 'tobi', display_name: 'Tobi Rahn', total_credits: 186, karma: 0, rank: 'thetes', preferred_disciplines: ['
|
|
45
|
-
[MARA]: { id: MARA, github_login: 'mara', display_name: 'Mara Okonkwo', total_credits: 2480, karma: 118, rank: 'metic', preferred_disciplines: ['
|
|
44
|
+
[INES]: { id: INES, github_login: 'ines', display_name: 'Ines Varga', total_credits: 312, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
|
|
45
|
+
[TOBI]: { id: TOBI, github_login: 'tobi', display_name: 'Tobi Rahn', total_credits: 186, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
|
|
46
|
+
[MARA]: { id: MARA, github_login: 'mara', display_name: 'Mara Okonkwo', total_credits: 2480, karma: 118, rank: 'metic', preferred_disciplines: ['artisan', 'artist'], main_discipline: 'artisan' },
|
|
46
47
|
[NEW]: { id: NEW, github_login: 'new', display_name: 'Newcomer', total_credits: 0, karma: 0, rank: 'xenos', preferred_disciplines: [], main_discipline: null },
|
|
47
|
-
[UI]: { id: UI, github_login: 'priya', display_name: 'Priya Das', total_credits: 60, karma: 0, rank: 'metic', preferred_disciplines: ['
|
|
48
|
+
[UI]: { id: UI, github_login: 'priya', display_name: 'Priya Das', total_credits: 60, karma: 0, rank: 'metic', preferred_disciplines: ['artisan', 'ui'], main_discipline: 'ui' },
|
|
48
49
|
[LEA]: { id: LEA, github_login: 'lea', display_name: 'Lea Brandt', total_credits: 30, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
|
|
49
50
|
};
|
|
50
51
|
const SHIPPED = { [INES]: 0, [TOBI]: 0, [MARA]: 64, [NEW]: 0, [UI]: 2, [LEA]: 0 };
|
|
51
52
|
|
|
52
53
|
// What CREDITS_BY_CRAFT_SQL returns, grouped by (builder, discipline). Ines's
|
|
53
54
|
// session reward was split across two tasks of different crafts, so her rows
|
|
54
|
-
// arrive as halves; the fold has to make them exact again.
|
|
55
|
+
// arrive as halves; the fold has to make them exact again. Her page credits
|
|
56
|
+
// (120) and her idea credits (42) both count as artist money since task 1005141;
|
|
57
|
+
// the statement tags the idea streams 'ideas' so the sky's idea total can still
|
|
58
|
+
// read them alone (credits-by-craft.js IDEAS), so they arrive as two rows.
|
|
55
59
|
const LEDGER_ROWS = [
|
|
56
60
|
{ builder_id: String(INES), discipline: 'artist', amount: 120 },
|
|
57
|
-
{ builder_id: String(INES), discipline: '
|
|
58
|
-
{ builder_id: String(INES), discipline: '
|
|
61
|
+
{ builder_id: String(INES), discipline: 'ideas', amount: 42 },
|
|
62
|
+
{ builder_id: String(INES), discipline: 'artisan', amount: 74.5 },
|
|
59
63
|
{ builder_id: String(INES), discipline: 'ui', amount: 74.5 },
|
|
60
64
|
{ builder_id: String(INES), discipline: 'other', amount: 1 },
|
|
61
|
-
{ builder_id: String(TOBI), discipline: '
|
|
62
|
-
{ builder_id: String(MARA), discipline: '
|
|
63
|
-
{ builder_id: String(MARA), discipline: '
|
|
65
|
+
{ builder_id: String(TOBI), discipline: 'ideas', amount: 186 },
|
|
66
|
+
{ builder_id: String(MARA), discipline: 'artisan', amount: 2460 },
|
|
67
|
+
{ builder_id: String(MARA), discipline: 'ideas', amount: 20 },
|
|
64
68
|
{ builder_id: String(UI), discipline: 'ui', amount: 60 },
|
|
65
|
-
// a discipline nobody mapped yet counts as
|
|
69
|
+
// a discipline nobody mapped yet counts as artisan work, never vanishes
|
|
66
70
|
{ builder_id: String(UI), discipline: 'some-module-craft', amount: 0 },
|
|
67
71
|
];
|
|
68
72
|
|
|
@@ -227,23 +231,26 @@ const profile = async (id) => {
|
|
|
227
231
|
|
|
228
232
|
// ---- the artist who has never shipped a task --------------------------------
|
|
229
233
|
|
|
230
|
-
await t('ARTIST, never shipped: leads with artist, by what she earned, and shows
|
|
234
|
+
await t('ARTIST, never shipped: leads with artist, by what she earned, and shows artisan beside it', async () => {
|
|
231
235
|
const { json } = await profile(INES);
|
|
232
236
|
assert.equal(json.shipped_count, 0, 'the fixture is the case the task names: no shipped task at all');
|
|
233
237
|
const r = json.roles;
|
|
234
238
|
assert.equal(r.main, 'artist');
|
|
235
239
|
assert.equal(r.main_basis, 'earned', 'no main role chosen yet, so it is inferred and says so');
|
|
236
|
-
assert.deepEqual(r.shown, ['artist', '
|
|
237
|
-
'main first, then every craft she EARNED in,
|
|
238
|
-
assert.equal(r.
|
|
240
|
+
assert.deepEqual(r.shown, ['artist', 'artisan'],
|
|
241
|
+
'main first, then every craft she EARNED in, ONCE each (her session reward was part artisan work)');
|
|
242
|
+
assert.equal(r.artisan.shipped, 0);
|
|
239
243
|
});
|
|
240
244
|
|
|
241
|
-
await t('ARTIST: pages approved, pages, N of M per surface,
|
|
245
|
+
await t('ARTIST: pages approved, pages, N of M per surface, the craft credits and this week', async () => {
|
|
242
246
|
const a = (await profile(INES)).json.roles.artist;
|
|
243
247
|
assert.equal(a.pages_approved, 4, 'four approved rounds, each one paid');
|
|
244
248
|
assert.equal(a.pages, 3, 'on three distinct pages (one was tweaked twice)');
|
|
245
249
|
assert.equal(a.surfaces_touched, 2);
|
|
246
|
-
assert.equal(a.credits,
|
|
250
|
+
assert.equal(a.credits, 236, 'the artist craft ledger share: pages (120) + ideas (42) + the ui half of her session (74)');
|
|
251
|
+
assert.equal(a.size.paid, 42, 'her ideas total is her idea streams alone, never her page credits (task 1005141)');
|
|
252
|
+
assert.equal(a.filed, 4, 'and the same block carries her ideas (task 1005141)');
|
|
253
|
+
assert.equal(a.unavailable, false);
|
|
247
254
|
assert.equal(a.this_week, 60, 'two approvals inside the trailing week');
|
|
248
255
|
const landing = a.surfaces.find((s) => s.id === 'landing');
|
|
249
256
|
const builders = a.surfaces.find((s) => s.id === 'builders');
|
|
@@ -258,30 +265,32 @@ await t('ARTIST: pages approved, pages, N of M per surface, credits from pages a
|
|
|
258
265
|
|
|
259
266
|
await t('the credit split is exact: halves rounded by largest remainder, ui folded into artist', async () => {
|
|
260
267
|
const c = (await profile(INES)).json.roles.credits_by_craft;
|
|
261
|
-
assert.deepEqual(c, {
|
|
268
|
+
assert.deepEqual(c, { artisan: 75, artist: 236, other: 1, total: 312 },
|
|
262
269
|
'74.5 + 74.5 must not round to 150 or 148; the parts equal the whole');
|
|
263
|
-
assert.equal(c.
|
|
270
|
+
assert.equal(c.artisan + c.artist + c.other, c.total);
|
|
264
271
|
});
|
|
265
272
|
|
|
266
|
-
// ---- the
|
|
273
|
+
// ---- the artist who only files ideas, and has never shipped a task -----------
|
|
267
274
|
|
|
268
|
-
await t('
|
|
275
|
+
await t('IDEAS-ONLY ARTIST, never shipped: leads with artist, shows nothing it did not earn', async () => {
|
|
269
276
|
const { json } = await profile(TOBI);
|
|
270
277
|
assert.equal(json.shipped_count, 0);
|
|
271
|
-
assert.equal(json.roles.main, '
|
|
272
|
-
assert.deepEqual(json.roles.shown, ['
|
|
273
|
-
assert.equal(json.roles.
|
|
274
|
-
assert.equal(json.roles.
|
|
278
|
+
assert.equal(json.roles.main, 'artist');
|
|
279
|
+
assert.deepEqual(json.roles.shown, ['artist']);
|
|
280
|
+
assert.equal(json.roles.artisan, null, 'no artisan block for someone with no artisan credit');
|
|
281
|
+
assert.equal(json.roles.ideator, undefined, 'there is no separate ideator craft any more');
|
|
282
|
+
assert.equal(json.roles.artist.pages_approved, 0, 'no pages approved is an answer, not a gap');
|
|
283
|
+
assert.equal(json.roles.artist.credits, 186, 'every one of them from ideas');
|
|
275
284
|
});
|
|
276
285
|
|
|
277
|
-
await t('
|
|
278
|
-
const i = (await profile(TOBI)).json.roles.
|
|
286
|
+
await t('ARTIST ideas: filed, ignited, Full Ideas, ratified, total size every stream + the estimate marker, still alive', async () => {
|
|
287
|
+
const i = (await profile(TOBI)).json.roles.artist;
|
|
279
288
|
assert.equal(i.filed, 8);
|
|
280
289
|
assert.equal(i.ignited, 5, 'the five that were paid for work, a burned-out one included');
|
|
281
290
|
assert.equal(i.full_ideas, 3);
|
|
282
291
|
assert.equal(i.ratified, 1, 'the one the board carried');
|
|
283
292
|
assert.deepEqual(i.size, { total: 196, paid: 186, estimated: 10, historical: true },
|
|
284
|
-
'the ledger
|
|
293
|
+
'the ledger artist column (every idea stream: Tobi has no pages) plus the historical estimate, marked');
|
|
285
294
|
const expected = (64 * 1 + 42 * 0.6 + 18 + 22 * 0 + 26 * 0.85) / (64 + 42 + 18 + 22 + 26);
|
|
286
295
|
assert.ok(Math.abs(i.alive.share - expected) < 1e-9, `size-weighted live share, got ${i.alive.share}`);
|
|
287
296
|
assert.equal(i.alive.has_work, true);
|
|
@@ -294,30 +303,31 @@ await t('the trail is read at the public rank, so a count never changes with who
|
|
|
294
303
|
assert.deepEqual(skyCalls, [{ builderId: TOBI, viewerRank: 'xenos' }]);
|
|
295
304
|
});
|
|
296
305
|
|
|
297
|
-
// ---- the
|
|
306
|
+
// ---- the artisan ------------------------------------------------------------
|
|
298
307
|
|
|
299
|
-
await t('
|
|
308
|
+
await t('ARTISAN: unchanged headline (works shipped), chosen main role, a little artist (one idea) beside it', async () => {
|
|
300
309
|
const r = (await profile(MARA)).json.roles;
|
|
301
|
-
assert.equal(r.main, '
|
|
310
|
+
assert.equal(r.main, 'artisan');
|
|
302
311
|
assert.equal(r.main_basis, 'chosen');
|
|
303
|
-
assert.deepEqual(r.shown, ['
|
|
304
|
-
assert.deepEqual(r.
|
|
305
|
-
assert.equal(r.artist,
|
|
306
|
-
assert.equal(r.
|
|
307
|
-
assert.equal(r.
|
|
312
|
+
assert.deepEqual(r.shown, ['artisan', 'artist']);
|
|
313
|
+
assert.deepEqual(r.artisan, { shipped: 64, credits: 2460 });
|
|
314
|
+
assert.equal(r.artist.pages_approved, 0, 'her artist credit is an idea, so the page count is a true zero');
|
|
315
|
+
assert.equal(r.artist.credits, 20);
|
|
316
|
+
assert.equal(r.artist.filed, 1);
|
|
317
|
+
assert.equal(r.artist.size.paid, 20);
|
|
308
318
|
});
|
|
309
319
|
|
|
310
320
|
// ---- the fallbacks and the boundary ------------------------------------------
|
|
311
321
|
|
|
312
|
-
await t('main role: a chosen ui reads as artist; nothing chosen and nothing earned is
|
|
322
|
+
await t('main role: a chosen ui reads as artist; nothing chosen and nothing earned is artisan, said to be a default', async () => {
|
|
313
323
|
const ui = (await profile(UI)).json.roles;
|
|
314
324
|
assert.equal(ui.main, 'artist');
|
|
315
325
|
assert.equal(ui.main_basis, 'chosen');
|
|
316
|
-
assert.deepEqual(ui.preferred, ['
|
|
326
|
+
assert.deepEqual(ui.preferred, ['artisan', 'artist']);
|
|
317
327
|
const nobody = (await profile(NEW)).json.roles;
|
|
318
|
-
assert.equal(nobody.main, '
|
|
328
|
+
assert.equal(nobody.main, 'artisan');
|
|
319
329
|
assert.equal(nobody.main_basis, 'default');
|
|
320
|
-
assert.deepEqual(nobody.shown, ['
|
|
330
|
+
assert.deepEqual(nobody.shown, ['artisan']);
|
|
321
331
|
assert.equal(nobody.credits_by_craft.total, 0, 'zero credits is an answer, not a gap');
|
|
322
332
|
});
|
|
323
333
|
|
|
@@ -336,8 +346,8 @@ await t('a missing port reads as unknown (null), never as zero', async () => {
|
|
|
336
346
|
seams.registerProvider('reward', { getBuilderAchievements: async () => [], creditsByCraft: (ids) => creditsByCraftLib.creditsByCraft(ids, { client: ledgerClient }) });
|
|
337
347
|
try {
|
|
338
348
|
const r = (await profile(TOBI)).json.roles;
|
|
339
|
-
assert.equal(r.
|
|
340
|
-
assert.equal(r.
|
|
349
|
+
assert.equal(r.artist.filed, null);
|
|
350
|
+
assert.equal(r.artist.unavailable, true);
|
|
341
351
|
} finally {
|
|
342
352
|
seams.registerProvider('ideas.ideatorStats', ideasPort);
|
|
343
353
|
}
|
|
@@ -354,7 +364,7 @@ await t('apportion hands out exactly the rounding left over, larger remainder fi
|
|
|
354
364
|
await t('the ledger statement classifies by reason, splits the session reward and escapes the artist prefix', () => {
|
|
355
365
|
const sql = creditsByCraftLib.CREDITS_BY_CRAFT_SQL;
|
|
356
366
|
assert.match(sql, /cl\.reason LIKE \$2 THEN 'artist'/);
|
|
357
|
-
assert.match(sql, /idea\.credit\.%' OR cl
|
|
367
|
+
assert.match(sql, /idea\.credit\.%' OR cl.reason = 'idea_promotion_bonus' THEN 'ideas'/);
|
|
358
368
|
assert.match(sql, /GREATEST\(COALESCE\(cardinality\(sr\.task_ids\), 0\), 1\)/, 'an even split, and a session that shipped nothing still counts once');
|
|
359
369
|
const last = ledgerSql[ledgerSql.length - 1];
|
|
360
370
|
assert.equal(last.params[1], 'tweak.page\\_approved:%', 'the underscore is escaped, so it is not a LIKE wildcard');
|
|
@@ -144,14 +144,14 @@ try {
|
|
|
144
144
|
await q(
|
|
145
145
|
`INSERT INTO platform_identity_builder_project_activity
|
|
146
146
|
(client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, last_active)
|
|
147
|
-
VALUES ($1, $6, 100, 5, 3, 'metic', '{
|
|
147
|
+
VALUES ($1, $6, 100, 5, 3, 'metic', '{artisan,ui}', '2026-01-01T00:00:00Z'),
|
|
148
148
|
($2, $6, 50, 2, 1, 'archon', '{artist}', '2026-02-01T00:00:00Z'),
|
|
149
149
|
($3, $6, 25, 1, 0, NULL, '{}', '2026-03-01T00:00:00Z'),
|
|
150
150
|
($4, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
|
|
151
151
|
($5, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
|
|
152
|
-
($1, $7, 60, 4, 2, 'metic', '{
|
|
153
|
-
($1, $8, 10, 1, 0, 'xenos', '{
|
|
154
|
-
($1, $9, 90, 3, 1, 'metic', '{
|
|
152
|
+
($1, $7, 60, 4, 2, 'metic', '{artisan}', '2026-01-01T00:00:00Z'),
|
|
153
|
+
($1, $8, 10, 1, 0, 'xenos', '{artisan}', '2026-01-01T00:00:00Z'),
|
|
154
|
+
($1, $9, 90, 3, 1, 'metic', '{artisan}', '2026-01-01T00:00:00Z')`,
|
|
155
155
|
[...ALL_CLIENTS, ACTIVE, HIDDEN, PROV, LEGACY],
|
|
156
156
|
);
|
|
157
157
|
|
|
@@ -170,7 +170,7 @@ try {
|
|
|
170
170
|
const r = await pi.getRollupForBuilder(ACTIVE, { pool });
|
|
171
171
|
assert.deepEqual(r.totals, { projects: 5, credits: 175, works_shipped: 8, karma: 4 });
|
|
172
172
|
pass++; console.log(' ok - totals aggregate un-fanned across the builder\'s projects');
|
|
173
|
-
assert.deepEqual([...r.disciplines].sort(), ['
|
|
173
|
+
assert.deepEqual([...r.disciplines].sort(), ['artisan', 'artist', 'ui']);
|
|
174
174
|
pass++; console.log(' ok - disciplines are the deduped union');
|
|
175
175
|
ok(new Date(r.last_active).toISOString() === '2026-03-01T00:00:00.000Z', 'last_active is the max');
|
|
176
176
|
ok(r.projects.length === 1 && r.projects[0].name === 'Pub Proj', 'only the on-the-map Public project is named — unfeatured, and named anyway');
|
|
@@ -240,7 +240,7 @@ try {
|
|
|
240
240
|
await pi.upsertActivity({
|
|
241
241
|
clientId: PUB_CLIENT, githubId: LAZY, credits: 1,
|
|
242
242
|
rank: 'Supreme Overlord',
|
|
243
|
-
disciplines: ['
|
|
243
|
+
disciplines: ['artisan', 42, 'x'.repeat(33), 'a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
|
244
244
|
}, { pool });
|
|
245
245
|
const { rows: [act] } = await q(
|
|
246
246
|
`SELECT rank_held, disciplines FROM platform_identity_builder_project_activity
|
|
@@ -249,6 +249,19 @@ try {
|
|
|
249
249
|
ok(act.disciplines.length <= 8 && act.disciplines.every((d) => d.length <= 32),
|
|
250
250
|
'disciplines land validated + capped');
|
|
251
251
|
|
|
252
|
+
// 6 — a reporting instance on a core older than the craft rename (task 1005141) still
|
|
253
|
+
// sends engineer / ideator. They land as today's crafts, folded and deduped, so the hub
|
|
254
|
+
// never shows an old name beside the new one (task 1005193).
|
|
255
|
+
await pi.upsertActivity({
|
|
256
|
+
clientId: PUB_CLIENT, githubId: LAZY, credits: 1,
|
|
257
|
+
disciplines: ['engineer', 'artist', 'ideator', 'ui', 'artisan'],
|
|
258
|
+
}, { pool });
|
|
259
|
+
const { rows: [folded] } = await q(
|
|
260
|
+
`SELECT disciplines FROM platform_identity_builder_project_activity
|
|
261
|
+
WHERE client_id = $1 AND github_id = $2`, [PUB_CLIENT, LAZY]);
|
|
262
|
+
assert.deepEqual(folded.disciplines, ['artisan', 'artist', 'ui']);
|
|
263
|
+
pass++; console.log(' ok - old craft names from an older core are folded to today\'s on ingest');
|
|
264
|
+
|
|
252
265
|
console.log(`\nprofile_rollup_consent.mjs: ${pass} passed`);
|
|
253
266
|
} finally {
|
|
254
267
|
await cleanup();
|
package/tests/profile_route.mjs
CHANGED
|
@@ -69,7 +69,7 @@ const FULL_ROW = {
|
|
|
69
69
|
deactivated_at: null,
|
|
70
70
|
reactivated_at: null,
|
|
71
71
|
deactivated_reason: SECRET_REASON,
|
|
72
|
-
preferred_disciplines: ['
|
|
72
|
+
preferred_disciplines: ['artisan'],
|
|
73
73
|
system_role: 'superadmin',
|
|
74
74
|
anthropic_email: SECRET_EMAIL,
|
|
75
75
|
anthropic_user_id: SECRET_USER,
|
|
@@ -107,8 +107,8 @@ function stubPublicProfileExtras() {
|
|
|
107
107
|
.map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held, recent_ships: [] }));
|
|
108
108
|
return {
|
|
109
109
|
totals: { projects: 2, credits: 150, works_shipped: 7, karma: 4 },
|
|
110
|
-
disciplines: ['
|
|
111
|
-
crafts: { artist: { pages_approved: 3, credits: 40 } },
|
|
110
|
+
disciplines: ['artisan', 'ui'],
|
|
111
|
+
crafts: { artist: { pages_approved: 3, filed: 2, full_ideas: null, ratified: null, credits: 40 } },
|
|
112
112
|
last_active: '2026-08-01T00:00:00.000Z',
|
|
113
113
|
projects: named,
|
|
114
114
|
more_projects: CROSS_PROJECT_ROWS.length - named.length,
|
|
@@ -118,11 +118,11 @@ function stubPublicProfileExtras() {
|
|
|
118
118
|
// crafts (task 1004440) is each craft's summed counts — always present, {} when
|
|
119
119
|
// no project reported any — so it belongs in the exact list too.
|
|
120
120
|
const CP_PUBLIC_KEYS = ['totals', 'disciplines', 'crafts', 'last_active', 'projects', 'more_projects'];
|
|
121
|
-
// The only shape crafts may take: the
|
|
121
|
+
// The only shape crafts may take: the two crafts, each a block of counts. The
|
|
122
|
+
// artist block carries the pages AND the ideas (task 1005141 folded the ideator in).
|
|
122
123
|
const CP_ROLE_FIELDS = {
|
|
123
|
-
|
|
124
|
-
artist: ['credits', 'pages_approved'],
|
|
125
|
-
ideator: ['credits', 'filed', 'full_ideas', 'ratified'],
|
|
124
|
+
artisan: ['credits', 'shipped'],
|
|
125
|
+
artist: ['credits', 'filed', 'full_ideas', 'pages_approved', 'ratified'],
|
|
126
126
|
};
|
|
127
127
|
// recent_ships rides the project row (task 1002803) and is ALWAYS present —
|
|
128
128
|
// getRollupForBuilder defaults it to [] — so it belongs in the exact key list.
|
|
@@ -139,7 +139,7 @@ function assertCrossProjectContract(cp, text) {
|
|
|
139
139
|
assert.deepEqual(Object.keys(cp).sort(), [...CP_PUBLIC_KEYS].sort(), 'cross_project keys are exactly the contract');
|
|
140
140
|
assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
|
|
141
141
|
for (const [craft, block] of Object.entries(cp.crafts)) {
|
|
142
|
-
assert.ok(CP_ROLE_FIELDS[craft], `crafts carries only the
|
|
142
|
+
assert.ok(CP_ROLE_FIELDS[craft], `crafts carries only the two crafts — found "${craft}"`);
|
|
143
143
|
assert.deepEqual(Object.keys(block).sort(), CP_ROLE_FIELDS[craft], `${craft} carries only its named counts`);
|
|
144
144
|
for (const v of Object.values(block)) assert.ok(v === null || Number.isInteger(v), 'a craft count is a number or null, never text');
|
|
145
145
|
}
|
|
@@ -393,7 +393,7 @@ const SCOUTING_TABLE = [
|
|
|
393
393
|
{ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
|
|
394
394
|
row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
|
|
395
395
|
project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
|
|
396
|
-
last_active: '2026-08-01T00:00:00.000Z', disciplines: ['
|
|
396
|
+
last_active: '2026-08-01T00:00:00.000Z', disciplines: ['artisan'] } },
|
|
397
397
|
{ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true,
|
|
398
398
|
row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
|
|
399
399
|
project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
|
|
@@ -405,7 +405,7 @@ const SCOUTING_TABLE = [
|
|
|
405
405
|
{ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false,
|
|
406
406
|
row: { github_id: '14', github_login: PRIVATE_LOGIN, display_name: 'Private', avatar_url: null,
|
|
407
407
|
project_count: 3, total_credits: 63127, total_tasks_shipped: 4409, total_karma: 2287,
|
|
408
|
-
last_active: '2026-08-04T00:00:00.000Z', disciplines: ['
|
|
408
|
+
last_active: '2026-08-04T00:00:00.000Z', disciplines: ['artisan'] } },
|
|
409
409
|
];
|
|
410
410
|
|
|
411
411
|
const engine = {
|
|
@@ -458,7 +458,7 @@ const engine = {
|
|
|
458
458
|
if (/AS works_shipped/.test(sql)) {
|
|
459
459
|
return { rows: [{ projects: PROOF_PROJECTS.length, credits: sumOf('credits'),
|
|
460
460
|
works_shipped: sumOf('tasks_shipped'), karma: sumOf('karma'),
|
|
461
|
-
last_active: '2026-08-02T00:00:00.000Z', disciplines: ['
|
|
461
|
+
last_active: '2026-08-02T00:00:00.000Z', disciplines: ['artisan', 'ui'] }] };
|
|
462
462
|
}
|
|
463
463
|
throw new Error(`fake engine: unexpected query ${sql.slice(0, 60)}`);
|
|
464
464
|
},
|
|
@@ -496,7 +496,7 @@ await t('R06.1 a two-project builder\'s COMBINED record shows to another account
|
|
|
496
496
|
assert.equal(cp.totals.credits, sumOf('credits'), 'credits are the sum across BOTH projects');
|
|
497
497
|
assert.equal(cp.totals.works_shipped, sumOf('tasks_shipped'), 'ships are summed across both');
|
|
498
498
|
assert.equal(cp.totals.karma, sumOf('karma'), 'karma is summed across both');
|
|
499
|
-
assert.deepEqual(cp.disciplines, ['
|
|
499
|
+
assert.deepEqual(cp.disciplines, ['artisan', 'ui'], 'the disciplines union spans both projects');
|
|
500
500
|
assert.equal(cp.projects.length, 1, 'only the Public project is NAMED — unfeatured, and named anyway');
|
|
501
501
|
assert.equal(cp.projects[0].name, PROOF_PROJECTS[0].name);
|
|
502
502
|
assert.equal(cp.more_projects, 1, 'the Private one is counted, never named — featured, and unnamed anyway');
|
|
@@ -37,12 +37,12 @@ const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.htm
|
|
|
37
37
|
const BUILDER = {
|
|
38
38
|
id: 7, github_login: 'pheidias', display_name: 'Pheidias', avatar_url: null,
|
|
39
39
|
rank: 'metic', total_credits: 120, karma: 3, created_at: '2026-01-01T00:00:00.000Z',
|
|
40
|
-
preferred_disciplines: ['
|
|
40
|
+
preferred_disciplines: ['artisan'],
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const CP_FULL = {
|
|
44
44
|
totals: { projects: 3, credits: 150, works_shipped: 7, karma: 4 },
|
|
45
|
-
disciplines: ['
|
|
45
|
+
disciplines: ['artisan', 'ui'],
|
|
46
46
|
last_active: '2026-08-01T00:00:00.000Z',
|
|
47
47
|
projects: [
|
|
48
48
|
{ name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic' },
|
|
@@ -84,7 +84,7 @@ async function boot({ search = '', me, profile } = {}) {
|
|
|
84
84
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
85
85
|
roleAxisLabel: () => 'Role',
|
|
86
86
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
87
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
87
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
88
88
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
89
89
|
ROLE_UNSET: 'unclassified',
|
|
90
90
|
$, escapeHtml,
|
|
@@ -172,7 +172,7 @@ test('full cross_project → tiles, named rows, plain-text standing, +N more', a
|
|
|
172
172
|
assert.ok(!html.includes('rank-badge'), 'never the local rank-badge component for another project\'s rank (spec Decision 1)');
|
|
173
173
|
assert.match(html, /\+ 1 more project/, 'non-public projects are counted, never named');
|
|
174
174
|
// task 1003936: one word for the axis, and the values are named not slugged.
|
|
175
|
-
assert.match(html, /Roles:
|
|
175
|
+
assert.match(html, /Roles: Artisan, UI/);
|
|
176
176
|
assert.match(html, /Last active 2026-08-01/);
|
|
177
177
|
assert.ok(!html.includes('/settings#cross-project'), 'no self link for a stranger');
|
|
178
178
|
});
|
|
@@ -34,8 +34,8 @@ function fakePool(rows) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const BUILDERS = [
|
|
37
|
-
{ github_id: 1, github_login: 'ada', display_name: 'Ada L', avatar_url: 'a.png', disciplines: ['
|
|
38
|
-
{ github_id: 2, github_login: 'grace', display_name: 'Grace H', avatar_url: 'g.png', disciplines: ['
|
|
37
|
+
{ github_id: 1, github_login: 'ada', display_name: 'Ada L', avatar_url: 'a.png', disciplines: ['artisan'], project_count: 3, total_tasks_shipped: 40, total_credits: 900 },
|
|
38
|
+
{ github_id: 2, github_login: 'grace', display_name: 'Grace H', avatar_url: 'g.png', disciplines: ['artisan'], project_count: 2, total_tasks_shipped: 30, total_credits: 800 },
|
|
39
39
|
{ github_id: 3, github_login: 'linus', display_name: null, avatar_url: null, disciplines: null, project_count: 1, total_tasks_shipped: 10, total_credits: 100 },
|
|
40
40
|
];
|
|
41
41
|
|
|
@@ -230,7 +230,7 @@ await test('BEHAVIOUR: a failed create throws with the status, it does not retur
|
|
|
230
230
|
// The hall promote once sent no discipline, and an idea whose wording names no
|
|
231
231
|
// craft was refused "discipline was not supplied and could not be inferred".
|
|
232
232
|
await test('BOTH PATHS: the hall promote payload always sends a discipline', () => {
|
|
233
|
-
assert.match(HALL, /discipline: \(form\.discipline && form\.discipline\.value\) \|\| '
|
|
233
|
+
assert.match(HALL, /discipline: \(form\.discipline && form\.discipline\.value\) \|\| 'artisan'/,
|
|
234
234
|
'hall: discipline must reach the create payload, defaulting to engineer');
|
|
235
235
|
assert.match(HALL, /<select name="discipline">/, 'hall: the promote form offers a Craft picker');
|
|
236
236
|
});
|