@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
|
@@ -117,18 +117,18 @@ function classifyTaskKind(title, description) {
|
|
|
117
117
|
// -------------------------------------------------------------------------
|
|
118
118
|
//
|
|
119
119
|
// Orthogonal to kind. Classifies the builder-interest axis (migration 086):
|
|
120
|
-
//
|
|
120
|
+
// artisan — the prompter is reasoning about algorithms, infra, data
|
|
121
121
|
// plumbing, dev tooling, scanners, schemas, integrations.
|
|
122
122
|
// The person guiding the prompt brings software-engineering
|
|
123
123
|
// experience.
|
|
124
|
-
// artist — producing or shipping visual artifacts
|
|
125
|
-
// generation, palettes, walk animations, aesthetic passes
|
|
126
|
-
//
|
|
127
|
-
//
|
|
124
|
+
// artist — producing or shipping visual artifacts (sprite/tile
|
|
125
|
+
// generation, palettes, walk animations, aesthetic passes)
|
|
126
|
+
// and, since task 1005141, the old ideator's lane too:
|
|
127
|
+
// thinking/writing/scoping, narrative, lore, briefs, naming.
|
|
128
128
|
// unclassified — signals weak or balanced; hand-triage.
|
|
129
129
|
//
|
|
130
130
|
// The scoring engine first decides engineer-vs-creative; a creative verdict
|
|
131
|
-
// is then split into artist/
|
|
131
|
+
// is then split into artist/ui by splitCreativeDiscipline().
|
|
132
132
|
//
|
|
133
133
|
// Calibrated against Lars's hand-classifications of 25 tasks (mig 014
|
|
134
134
|
// backfill, 2026-05-12). Key insights from those decisions:
|
|
@@ -148,11 +148,11 @@ function classifyTaskKind(title, description) {
|
|
|
148
148
|
// summed and capped. The 0.4 description weight is what stops incidental
|
|
149
149
|
// creative phrasing in long descriptions from flipping engineering tasks.
|
|
150
150
|
|
|
151
|
-
// Migration 086 split the old "creative" lane into
|
|
152
|
-
// production) and
|
|
153
|
-
//
|
|
154
|
-
// engineer-vs-creative; a creative verdict
|
|
155
|
-
// artist/
|
|
151
|
+
// Migration 086 split the old "creative" lane into artist (visual asset
|
|
152
|
+
// production) and ideator (thinking/writing/scoping); task 1005141 folded the
|
|
153
|
+
// ideator back into the artist craft and renamed engineer → artisan. The binary
|
|
154
|
+
// scoring engine below still decides engineer-vs-creative; a creative verdict
|
|
155
|
+
// is then disambiguated into artist/ui by splitCreativeDiscipline(). When that split is unclear,
|
|
156
156
|
// it returns 'unclassified' (hand-triage) — matching the #615 backfill rule.
|
|
157
157
|
//
|
|
158
158
|
// Migration 176 (task 1376) added 'ui' — UI/interface DESIGN work (Figma,
|
|
@@ -160,7 +160,7 @@ function classifyTaskKind(title, description) {
|
|
|
160
160
|
// pixel-art `artist` lane (ADR 0079): producing interface designs is a
|
|
161
161
|
// distinct discipline from producing pixel-art assets, and routes to its own
|
|
162
162
|
// /design work-mode (discipline-modes.json) instead of /paint.
|
|
163
|
-
const VALID_DISCIPLINES = new Set(['
|
|
163
|
+
const VALID_DISCIPLINES = new Set(['artisan', 'artist', 'ui', 'unclassified']);
|
|
164
164
|
|
|
165
165
|
// The disciplines a WRITE may set (task 1002988). 'unclassified' is deliberately
|
|
166
166
|
// absent: it is a triage state, not a craft, and a create that lands one produces
|
|
@@ -169,12 +169,18 @@ const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'uncla
|
|
|
169
169
|
// ?discipline=unclassified filter keep resolving while the backfill runs.
|
|
170
170
|
// 'ui' stays: ADR 0272 keeps it as a module-contributed discipline, and the owner
|
|
171
171
|
// reconfirmed that on 2026-09-17 when abandoning task 1002987.
|
|
172
|
-
const WRITABLE_DISCIPLINES = new Set(['
|
|
172
|
+
const WRITABLE_DISCIPLINES = new Set(['artisan', 'artist', 'ui']);
|
|
173
173
|
|
|
174
174
|
// Mirrors the `kind` branch in the create routes: act on the classifier only when
|
|
175
175
|
// it is at least half sure, otherwise treat it as no answer at all.
|
|
176
176
|
const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
|
|
177
177
|
|
|
178
|
+
// The one place this module reads an old craft spelling as today's (task 1005141):
|
|
179
|
+
// engineer → artisan, ideator → artist. 'unclassified' and 'ui' pass through.
|
|
180
|
+
function normalizeDisciplineValue(d) {
|
|
181
|
+
return typeof d === 'string' ? require('../../src/module-api').crafts.normalizeCraft(d) : d;
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
// Resolve the discipline for a task WRITE. Honours an explicit valid value,
|
|
179
185
|
// otherwise INFERS from title + description + touches, and reports failure so the
|
|
180
186
|
// caller can 400 — per the owner decision of 2026-09-17, a bare 400 on a missing
|
|
@@ -186,6 +192,8 @@ const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
|
|
|
186
192
|
// have left the other quietly minting the very rows this task exists to remove.
|
|
187
193
|
// Returns { discipline, inferred?, confidence? } or { error, unclassifiable? }.
|
|
188
194
|
function resolveWriteDiscipline({ discipline, title, description, touches } = {}) {
|
|
195
|
+
// An old spelling ('engineer', 'ideator') is today's craft, not a bad one (task 1005141).
|
|
196
|
+
discipline = normalizeDisciplineValue(discipline);
|
|
189
197
|
if (discipline != null && discipline !== 'unclassified') {
|
|
190
198
|
return WRITABLE_DISCIPLINES.has(discipline) ? { discipline } : { error: 'bad_discipline' };
|
|
191
199
|
}
|
|
@@ -228,10 +236,10 @@ const TASK_FIELD_BOUNDS = {
|
|
|
228
236
|
const VOTABLE_TASK_STATUSES = new Set(['confirmed', 'shipped']);
|
|
229
237
|
|
|
230
238
|
// Disambiguation signals for the creative lane (migration 086). Visual
|
|
231
|
-
// art-production cues
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
239
|
+
// art-production cues and narrative / design-thinking / writing cues are both
|
|
240
|
+
// the artist craft since task 1005141 (the ideator folded into it); the two
|
|
241
|
+
// lists stay separate so each reads as what it detects. Scored by distinct-rule
|
|
242
|
+
// hit count over title+description, plus touches[] path prefixes.
|
|
235
243
|
const ARTIST_SIGNALS = [
|
|
236
244
|
/\bpixel[-\s]?art\b/i,
|
|
237
245
|
/\btile-?sheets?\b/i,
|
|
@@ -278,23 +286,24 @@ const UI_PATH_PREFIXES = [
|
|
|
278
286
|
'docs/design/',
|
|
279
287
|
];
|
|
280
288
|
|
|
281
|
-
// Split a 'creative' verdict into 'artist' | '
|
|
282
|
-
//
|
|
283
|
-
//
|
|
289
|
+
// Split a 'creative' verdict into 'artist' | 'ui' | 'unclassified'.
|
|
290
|
+
// The art and ideas signals are summed into ONE artist score (one craft since
|
|
291
|
+
// task 1005141 — scoring them as two lanes would call an even art/ideas task a
|
|
292
|
+
// tie and send it to hand-triage); it competes with the ui score. The stronger
|
|
293
|
+
// lane wins; a tie (including 0-0) → hand-triage.
|
|
284
294
|
function splitCreativeDiscipline(title, description, touches) {
|
|
285
295
|
const text = `${title || ''} ${description || ''}`;
|
|
286
296
|
const artist = ARTIST_SIGNALS.filter((re) => re.test(text)).length
|
|
287
|
-
+ countTouchesHits(touches, ARTIST_PATH_PREFIXES)
|
|
288
|
-
|
|
289
|
-
|
|
297
|
+
+ countTouchesHits(touches, ARTIST_PATH_PREFIXES)
|
|
298
|
+
+ IDEATOR_SIGNALS.filter((re) => re.test(text)).length
|
|
299
|
+
+ countTouchesHits(touches, IDEATOR_PATH_PREFIXES);
|
|
290
300
|
const ui = UI_SIGNALS.filter((re) => re.test(text)).length
|
|
291
301
|
+ countTouchesHits(touches, UI_PATH_PREFIXES);
|
|
292
302
|
const ranked = [
|
|
293
303
|
{ d: 'artist', n: artist },
|
|
294
|
-
{ d: 'ideator', n: ideator },
|
|
295
304
|
{ d: 'ui', n: ui },
|
|
296
305
|
].sort((a, b) => b.n - a.n);
|
|
297
|
-
// No signal anywhere, or the
|
|
306
|
+
// No signal anywhere, or the two lanes are tied → hand-triage.
|
|
298
307
|
if (ranked[0].n === 0 || ranked[0].n === ranked[1].n) return 'unclassified';
|
|
299
308
|
return ranked[0].d;
|
|
300
309
|
}
|
|
@@ -326,7 +335,7 @@ const CREATIVE_TEXT_RULES = [
|
|
|
326
335
|
// UI/interface DESIGN cues (mig 176 / task 1376) — route the creative verdict
|
|
327
336
|
// to the `ui` sub-lane via splitCreativeDiscipline. Kept here (not engineering)
|
|
328
337
|
// so "UI redesign of the hall" / "wireframe the checkout" land creative-first;
|
|
329
|
-
// splitCreativeDiscipline then picks `ui` over `artist
|
|
338
|
+
// splitCreativeDiscipline then picks `ui` over `artist`.
|
|
330
339
|
{ re: /\b(UI|UX)\s+(design|redesign|pass|polish|refresh|overhaul)\b/i, weight: 0.8 },
|
|
331
340
|
{ re: /\b(user\s+(interface|experience)|interface\s+design|wireframes?|mock-?ups?)\b/i, weight: 0.8 },
|
|
332
341
|
{ re: /\b(figma|claude[-\s]?design)\b.*\b(redesign|design|layout|screen|mock-?up|wireframe)\b/i, weight: 0.8 },
|
|
@@ -503,7 +512,7 @@ function classifyTaskDiscipline(title, description, touches) {
|
|
|
503
512
|
const d = splitCreativeDiscipline(titleText, descText, touches);
|
|
504
513
|
return { discipline: d, confidence: d === 'unclassified' ? 0 : cT.max };
|
|
505
514
|
}
|
|
506
|
-
return { discipline: '
|
|
515
|
+
return { discipline: 'artisan', confidence: eT.max };
|
|
507
516
|
}
|
|
508
517
|
// (b) Same max-weight (e.g. both 0.85). Combine title-rule count and
|
|
509
518
|
// touches[] hit count into a single asymmetry score — a side that
|
|
@@ -517,7 +526,7 @@ function classifyTaskDiscipline(title, description, touches) {
|
|
|
517
526
|
const d = splitCreativeDiscipline(titleText, descText, touches);
|
|
518
527
|
return { discipline: d, confidence: d === 'unclassified' ? 0 : 0.6 + Math.min(0.3, cAsym * 0.05) };
|
|
519
528
|
}
|
|
520
|
-
return { discipline: '
|
|
529
|
+
return { discipline: 'artisan', confidence: 0.6 + Math.min(0.3, -cAsym * 0.05) };
|
|
521
530
|
}
|
|
522
531
|
return { discipline: 'unclassified', confidence: 0 };
|
|
523
532
|
}
|
|
@@ -541,7 +550,7 @@ function classifyTaskDiscipline(title, description, touches) {
|
|
|
541
550
|
const d = splitCreativeDiscipline(titleText, descText, touches);
|
|
542
551
|
return { discipline: d, confidence: d === 'unclassified' ? 0 : creative };
|
|
543
552
|
}
|
|
544
|
-
return { discipline: '
|
|
553
|
+
return { discipline: 'artisan', confidence: engineering };
|
|
545
554
|
}
|
|
546
555
|
|
|
547
556
|
module.exports = {
|
|
@@ -550,6 +559,7 @@ module.exports = {
|
|
|
550
559
|
buildBlockerVendorRe,
|
|
551
560
|
VALID_KINDS,
|
|
552
561
|
VALID_DISCIPLINES,
|
|
562
|
+
normalizeDisciplineValue,
|
|
553
563
|
WRITABLE_DISCIPLINES,
|
|
554
564
|
resolveWriteDiscipline,
|
|
555
565
|
VOTABLE_TASK_STATUSES,
|
|
@@ -41,7 +41,7 @@ const shipped = (over = {}) => ({
|
|
|
41
41
|
version_id: 'BONGOS-V1',
|
|
42
42
|
goal_id: '1000074',
|
|
43
43
|
title: 'R09 · Reword the hall empty states',
|
|
44
|
-
discipline: '
|
|
44
|
+
discipline: 'artisan',
|
|
45
45
|
kind: 'feature',
|
|
46
46
|
status: 'shipped',
|
|
47
47
|
...over,
|
|
@@ -51,7 +51,7 @@ const shipped = (over = {}) => ({
|
|
|
51
51
|
const REVIEW_RULE = {
|
|
52
52
|
key: 'test-artist-review',
|
|
53
53
|
discipline: 'artist',
|
|
54
|
-
when: (t) => t.discipline === '
|
|
54
|
+
when: (t) => t.discipline === 'artisan',
|
|
55
55
|
title: (t) => `Review the copy that shipped in task ${t.id}`,
|
|
56
56
|
body: (t) => `Look at what ${t.title} put in front of a person.`,
|
|
57
57
|
};
|
|
@@ -195,7 +195,7 @@ await test('the production table is valid, and a duplicate key is refused', () =
|
|
|
195
195
|
|
|
196
196
|
const shippedCopy = (over = {}) => ({
|
|
197
197
|
id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
|
|
198
|
-
title: 'Reword the hall empty states', discipline: '
|
|
198
|
+
title: 'Reword the hall empty states', discipline: 'artisan',
|
|
199
199
|
kind: 'feature', status: 'shipped', module_key: 'hall-ui', source: 'manual',
|
|
200
200
|
...over,
|
|
201
201
|
});
|
|
@@ -262,8 +262,8 @@ await test('"copy or visuals" is a NARROW predicate — three ways in, and a def
|
|
|
262
262
|
assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
|
|
263
263
|
assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
|
|
264
264
|
// The default is NO — a backend/infra ship is not copy/visual work.
|
|
265
|
-
assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: '
|
|
266
|
-
assert.equal(H(shippedCopy({ module_key: null, discipline: '
|
|
265
|
+
assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'artisan', source: 'manual' })), false);
|
|
266
|
+
assert.equal(H(shippedCopy({ module_key: null, discipline: 'artisan', source: 'manual' })), false);
|
|
267
267
|
assert.equal(H(null), false);
|
|
268
268
|
});
|
|
269
269
|
|
|
@@ -303,7 +303,7 @@ await test('the runner still hands the artist gate to the rules, and a reader th
|
|
|
303
303
|
|
|
304
304
|
await test('a page file changed under modules/*/public/ is visual work whatever the label says', () => {
|
|
305
305
|
const H = cascade.hasCopyOrVisualWork;
|
|
306
|
-
const unlabelled = shippedCopy({ module_key: null, discipline: '
|
|
306
|
+
const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
|
|
307
307
|
assert.equal(H(unlabelled), false, 'the label alone says no — the gap task 1003480 fell through');
|
|
308
308
|
for (const p of [
|
|
309
309
|
'modules/hall-ui/public/goal-forest-lib.js',
|
|
@@ -316,7 +316,7 @@ await test('a page file changed under modules/*/public/ is visual work whatever
|
|
|
316
316
|
|
|
317
317
|
await test('a backend file in a UI module is NOT visual work — page files only', () => {
|
|
318
318
|
const H = cascade.hasCopyOrVisualWork;
|
|
319
|
-
const unlabelled = shippedCopy({ module_key: null, discipline: '
|
|
319
|
+
const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
|
|
320
320
|
assert.equal(H(unlabelled, [
|
|
321
321
|
'modules/hall-ui/module.json',
|
|
322
322
|
'modules/public-landing/routes/hub.js',
|
|
@@ -55,6 +55,9 @@ const KEYS = Object.freeze({
|
|
|
55
55
|
// When the planet came alive (founding switched off). With STARTED_AT it bounds the
|
|
56
56
|
// window a builder must have joined in to carry the founding mark (task 1004424).
|
|
57
57
|
ENDED_AT: 'founding.ended_at',
|
|
58
|
+
// When the owner skipped genesis instead of finishing it (genesis-home.js skipGenesis). Write-once, so the
|
|
59
|
+
// record says the planet came alive by skip, not by the five stages.
|
|
60
|
+
SKIPPED_AT: 'founding.skipped_at',
|
|
58
61
|
// Genesis is OFF unless a project turns it on (owner, 2026-11-01: not polished enough, so every
|
|
59
62
|
// project opens as the full hall). Read as ordinary 'true'; absent or anything else is OFF, which is
|
|
60
63
|
// also what pauses a project that was already mid-founding. A demo, or a project born with genesis,
|
|
@@ -426,6 +426,26 @@ async function comeAlive(builderId, deps = {}) {
|
|
|
426
426
|
return { ok: true };
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// skipGenesis — the owner's way out of genesis from ANY stage (incl. decide), with no Board
|
|
430
|
+
// Room sitting and no finished-tasks gate. Founding mode switches off exactly as comeAlive
|
|
431
|
+
// does (ended_at stamped, the full hall opens), and the skip is recorded once in
|
|
432
|
+
// founding.skipped_at with the stage it left, so the record shows it was skipped, not
|
|
433
|
+
// finished. Genesis still happens once (spec D8): a project that is already alive refuses.
|
|
434
|
+
// Filed genesis tasks stay on the board; nothing is deleted.
|
|
435
|
+
async function skipGenesis(builderId, deps = {}) {
|
|
436
|
+
const s = seams(deps);
|
|
437
|
+
const ended = await s.get(fm.KEYS.ENDED_AT);
|
|
438
|
+
if (ended && ended.value) return { ok: false, reason: 'already_alive' };
|
|
439
|
+
const mode = await fm.foundingModeFor(modeDeps(s));
|
|
440
|
+
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
441
|
+
const prior = await s.get(fm.KEYS.SKIPPED_AT);
|
|
442
|
+
if (!prior || !prior.value) {
|
|
443
|
+
await s.set(fm.KEYS.SKIPPED_AT, JSON.stringify({ at: (s.now || new Date()).toISOString(), stage: mode.stage }), builderId);
|
|
444
|
+
}
|
|
445
|
+
await fm.setFoundingMode({ active: false }, builderId, modeDeps(s));
|
|
446
|
+
return { ok: true, skipped: true, from_stage: mode.stage };
|
|
447
|
+
}
|
|
448
|
+
|
|
429
449
|
// onStagePassed — the `board.genesis_stage.passed` listener. Best-effort: the next read
|
|
430
450
|
// reconciles anyway.
|
|
431
451
|
async function onStagePassed(deps = {}) {
|
|
@@ -434,7 +454,7 @@ async function onStagePassed(deps = {}) {
|
|
|
434
454
|
|
|
435
455
|
module.exports = {
|
|
436
456
|
PLAN_KEY, STAGE_INFO, PROPER, DEMO_PEOPLE, DEMO_HOURS, intIn, parsePlan, shapeGenesis, demoMinutes, demoPlan,
|
|
437
|
-
readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, onStagePassed,
|
|
457
|
+
readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, skipGenesis, onStagePassed,
|
|
438
458
|
// the founding-mode deps this file reads the mode with, for founding-birth.js (task 1004506)
|
|
439
459
|
modeDepsFor: (deps) => modeDeps(seams(deps)),
|
|
440
460
|
};
|
|
@@ -186,4 +186,6 @@ function mountGenesisRoutes(router) {
|
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
router.post('/founding/alive', api.requireBuilder, owns, handle((req) => genesis.comeAlive(req.builder.id)));
|
|
189
|
+
// Skip genesis: the owner opens the full hall from any stage, no sitting, no gate.
|
|
190
|
+
router.post('/founding/skip', api.requireBuilder, owns, handle((req) => genesis.skipGenesis(req.builder.id)));
|
|
189
191
|
}
|
|
@@ -91,10 +91,11 @@ function referencedImageIds(bodyMd) {
|
|
|
91
91
|
// the ideas are triaged like any other, and nothing here grants anything.
|
|
92
92
|
const FENCE_LOOKALIKE_RE = /~{4,}[A-Za-z0-9-]*~{4,}/g;
|
|
93
93
|
// Two writers share the split: the artist's feedback doc (the palette) and the
|
|
94
|
-
//
|
|
94
|
+
// artist's notes (the lightbulb, task 1004498; the ideator folded into the artist in
|
|
95
|
+
// task 1005141, so the second voice is named for what it writes). Only the framing words differ.
|
|
95
96
|
const VOICES = {
|
|
96
97
|
artist: { who: 'artist', what: 'feedback about one page of a web app', ask: 'separate change the artist asks for or problem they point at', label: 'ARTIST-FEEDBACK', doc: 'doc', split: 'feedback' },
|
|
97
|
-
|
|
98
|
+
notes: { who: 'artist', what: 'notes, written while looking at one page of a web app', ask: 'separate idea, change or problem the artist raises', label: 'IDEATOR-NOTES', doc: 'notes', split: 'notes' },
|
|
98
99
|
};
|
|
99
100
|
function buildSplitPrompt({ pageTitle, pageId, bodyMd, voice = 'artist', nonce = crypto.randomBytes(8).toString('hex') }) {
|
|
100
101
|
const v = VOICES[voice] || VOICES.artist;
|
|
@@ -76,7 +76,7 @@ async function fileNote(note, { grade, capture, db, log } = {}) {
|
|
|
76
76
|
// an empty list means nothing filed and the caller says so.
|
|
77
77
|
async function fileIdeatorNotes(submit, { grade, capture, log, authorId, authorName } = {}) {
|
|
78
78
|
const page = { page_id: submit.page_id, page_path: submit.page_path, page_title: submit.page_title, body_md: submit.body_md };
|
|
79
|
-
let ideas = await aiIdeas({ note: page, grade, voice: '
|
|
79
|
+
let ideas = await aiIdeas({ note: page, grade, voice: 'notes', timeoutMs: IDEATOR_TIMEOUT_MS });
|
|
80
80
|
let splitBy = 'ai';
|
|
81
81
|
if (!ideas) { ideas = [N.wholeDocIdea({ pageTitle: page.page_title, pageId: page.page_id, bodyMd: page.body_md })]; splitBy = 'whole'; }
|
|
82
82
|
const filed = [];
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
// A reporting instance sends, per craft the builder has EARNED credits in, that
|
|
6
6
|
// craft's headline counts (src/bongos/auth-admission.js readCraftRollup):
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
// artist { pages_approved, credits }
|
|
10
|
-
//
|
|
8
|
+
// artisan { shipped, credits }
|
|
9
|
+
// artist { pages_approved, filed, full_ideas, ratified, credits }
|
|
10
|
+
//
|
|
11
|
+
// (the old engineer / artist / ideator shape is folded in at ingest, foldLegacy)
|
|
11
12
|
//
|
|
12
13
|
// Two halves live here, next to each other so they cannot disagree on the shape:
|
|
13
14
|
//
|
|
14
15
|
// sanitizeCrafts — what upsertActivity BINDS. The report is third-party
|
|
15
16
|
// self-report (ADR 0171 D7), so the shape and the caps are the hub's, not
|
|
16
|
-
// the sender's: only the
|
|
17
|
+
// the sender's: only the two crafts, only the named counts, each a
|
|
17
18
|
// non-negative integer under a ceiling or null. Anything else is dropped.
|
|
18
19
|
// A count is never text, so nothing here can carry a title or wording.
|
|
19
20
|
//
|
|
@@ -30,10 +31,36 @@
|
|
|
30
31
|
'use strict';
|
|
31
32
|
|
|
32
33
|
const CRAFT_FIELDS = Object.freeze({
|
|
33
|
-
|
|
34
|
-
artist: Object.freeze(['pages_approved', 'credits']),
|
|
35
|
-
ideator: Object.freeze(['filed', 'full_ideas', 'ratified', 'credits']),
|
|
34
|
+
artisan: Object.freeze(['shipped', 'credits']),
|
|
35
|
+
artist: Object.freeze(['pages_approved', 'filed', 'full_ideas', 'ratified', 'credits']),
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
// A reporting instance on a core older than task 1005141 still sends the old
|
|
39
|
+
// crafts: `engineer`, and `artist` + `ideator` as two blocks. They are folded
|
|
40
|
+
// into today's shape BEFORE the hub's caps run, so an old report and a new one
|
|
41
|
+
// land identically: engineer becomes artisan, and the ideator's counts join the
|
|
42
|
+
// artist block with the two credit figures summed. A craft already reported under
|
|
43
|
+
// its new name wins over an old-name block for the same craft.
|
|
44
|
+
function foldLegacy(raw) {
|
|
45
|
+
const out = { ...raw };
|
|
46
|
+
if (out.engineer && !out.artisan) out.artisan = out.engineer;
|
|
47
|
+
delete out.engineer;
|
|
48
|
+
const ideator = out.ideator;
|
|
49
|
+
delete out.ideator;
|
|
50
|
+
if (ideator && typeof ideator === 'object' && !Array.isArray(ideator)) {
|
|
51
|
+
const art = out.artist && typeof out.artist === 'object' && !Array.isArray(out.artist) ? out.artist : {};
|
|
52
|
+
// Each half is capped BEFORE the sum: a hostile -3 on one side must not
|
|
53
|
+
// quietly shave the other side's honest count.
|
|
54
|
+
const a = cleanCount(art.credits);
|
|
55
|
+
const i = cleanCount(ideator.credits);
|
|
56
|
+
out.artist = {
|
|
57
|
+
...art,
|
|
58
|
+
filed: ideator.filed, full_ideas: ideator.full_ideas, ratified: ideator.ratified,
|
|
59
|
+
credits: a == null && i == null ? null : (a || 0) + (i || 0),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
37
64
|
// Far above any honest count, low enough that a sum over every project stays a
|
|
38
65
|
// safe integer and a hostile report cannot dominate a profile by one number.
|
|
39
66
|
const MAX_CRAFT_COUNT = 10_000_000;
|
|
@@ -45,6 +72,7 @@ function cleanCount(v) {
|
|
|
45
72
|
function sanitizeCrafts(raw) {
|
|
46
73
|
const out = {};
|
|
47
74
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
|
|
75
|
+
raw = foldLegacy(raw);
|
|
48
76
|
for (const [craft, fields] of Object.entries(CRAFT_FIELDS)) {
|
|
49
77
|
const block = raw[craft];
|
|
50
78
|
if (!block || typeof block !== 'object' || Array.isArray(block)) continue;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- platform_identity_036 — the hub's per-project activity rollup follows core_296's craft
|
|
2
|
+
-- rename (task 1005141, BONGOS-V3 C1): engineer → artisan, ideator → artist.
|
|
3
|
+
--
|
|
4
|
+
-- `disciplines` is mapped and deduped. `crafts` (platform_identity_029) is keyed by
|
|
5
|
+
-- craft: the engineer block becomes the artisan block, and the ideator block's counts
|
|
6
|
+
-- (filed, full_ideas, ratified) merge into the artist block, with credits summed —
|
|
7
|
+
-- the shape modules/platform-identity/craft-rollup.js now sanitizes. A report from an
|
|
8
|
+
-- instance still on an older core is folded the same way at ingest, so these rows and
|
|
9
|
+
-- fresh reports agree.
|
|
10
|
+
|
|
11
|
+
BEGIN;
|
|
12
|
+
|
|
13
|
+
UPDATE platform_identity_builder_project_activity a
|
|
14
|
+
SET disciplines = ARRAY(
|
|
15
|
+
SELECT m.craft FROM (
|
|
16
|
+
SELECT CASE d WHEN 'engineer' THEN 'artisan' WHEN 'ideator' THEN 'artist' ELSE d END AS craft,
|
|
17
|
+
min(ord) AS first_seen
|
|
18
|
+
FROM unnest(a.disciplines) WITH ORDINALITY AS u(d, ord)
|
|
19
|
+
GROUP BY 1
|
|
20
|
+
) m ORDER BY m.first_seen)
|
|
21
|
+
WHERE a.disciplines && ARRAY['engineer', 'ideator']::text[];
|
|
22
|
+
|
|
23
|
+
UPDATE platform_identity_builder_project_activity
|
|
24
|
+
SET crafts = (crafts - 'engineer') || jsonb_build_object('artisan', crafts -> 'engineer')
|
|
25
|
+
WHERE crafts ? 'engineer' AND NOT crafts ? 'artisan';
|
|
26
|
+
|
|
27
|
+
UPDATE platform_identity_builder_project_activity
|
|
28
|
+
SET crafts = (crafts - 'ideator') || jsonb_build_object('artist',
|
|
29
|
+
coalesce(crafts -> 'artist', '{}'::jsonb)
|
|
30
|
+
|| jsonb_strip_nulls(jsonb_build_object(
|
|
31
|
+
'filed', crafts -> 'ideator' -> 'filed',
|
|
32
|
+
'full_ideas', crafts -> 'ideator' -> 'full_ideas',
|
|
33
|
+
'ratified', crafts -> 'ideator' -> 'ratified'))
|
|
34
|
+
|| jsonb_build_object('credits',
|
|
35
|
+
CASE WHEN jsonb_typeof(crafts -> 'artist' -> 'credits') = 'number'
|
|
36
|
+
OR jsonb_typeof(crafts -> 'ideator' -> 'credits') = 'number'
|
|
37
|
+
THEN to_jsonb(coalesce((crafts -> 'artist' ->> 'credits')::bigint, 0)
|
|
38
|
+
+ coalesce((crafts -> 'ideator' ->> 'credits')::bigint, 0))
|
|
39
|
+
ELSE 'null'::jsonb END))
|
|
40
|
+
WHERE crafts ? 'ideator';
|
|
41
|
+
|
|
42
|
+
INSERT INTO schema_migrations (version) VALUES ('platform_identity_036_crafts_artisan_artist')
|
|
43
|
+
ON CONFLICT (version) DO NOTHING;
|
|
44
|
+
|
|
45
|
+
COMMIT;
|
|
@@ -671,8 +671,8 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
|
|
|
671
671
|
// api.LIVE_RANK_LADDER is a published doorway export (src/module-api.js, a
|
|
672
672
|
// lazy getter over db-kernel's canonical four-rank ladder).
|
|
673
673
|
const safeRank = api.LIVE_RANK_LADDER.includes(rank) ? rank : null;
|
|
674
|
-
const disc = (Array.isArray(disciplines) ? disciplines : [])
|
|
675
|
-
.filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH)
|
|
674
|
+
const disc = api.crafts.normalizeCrafts((Array.isArray(disciplines) ? disciplines : []) // an older core's engineer/ideator → today's crafts, as foldLegacy (task 1005193)
|
|
675
|
+
.filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH))
|
|
676
676
|
.slice(0, MAX_ROLLUP_DISCIPLINES);
|
|
677
677
|
const ships = sanitizeRecentShips(recentShips, clientOrigin);
|
|
678
678
|
// Each craft's own counts (task 1004440), shaped and capped here — see craft-rollup.js.
|
|
@@ -55,7 +55,8 @@ module.exports = function communityLeaderboardRoutes() {
|
|
|
55
55
|
if (!leaderboard.isWindow(window)) {
|
|
56
56
|
return res.fail('bad_window', { status: 400, message: `window must be one of: ${leaderboard.WINDOWS.join(', ')}` });
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
// An old link's engineer/ideator reads as today's craft (task 1005141).
|
|
59
|
+
const discipline = require('../../../src/module-api').crafts.normalizeCraft((typeof req.query.discipline === 'string' ? req.query.discipline : '').trim());
|
|
59
60
|
if (discipline.length > leaderboard.MAX_DISCIPLINE_CHARS) {
|
|
60
61
|
return res.fail('discipline_too_long', { status: 400, message: `discipline must be at most ${leaderboard.MAX_DISCIPLINE_CHARS} characters` });
|
|
61
62
|
}
|
|
@@ -63,7 +63,8 @@ module.exports = function communitySearchRoutes() {
|
|
|
63
63
|
if (q.length > builderSearch.MAX_QUERY_CHARS) {
|
|
64
64
|
return res.fail('query_too_long', { status: 400, message: `q must be at most ${builderSearch.MAX_QUERY_CHARS} characters` });
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
// An old link's engineer/ideator reads as today's craft (task 1005141).
|
|
67
|
+
const discipline = require('../../../src/module-api').crafts.normalizeCraft((typeof req.query.discipline === 'string' ? req.query.discipline : '').trim());
|
|
67
68
|
if (discipline.length > builderSearch.MAX_DISCIPLINE_CHARS) {
|
|
68
69
|
return res.fail('discipline_too_long', { status: 400, message: `discipline must be at most ${builderSearch.MAX_DISCIPLINE_CHARS} characters` });
|
|
69
70
|
}
|
|
@@ -691,7 +691,8 @@ module.exports = function ssoRoutes() {
|
|
|
691
691
|
tasksShipped: b.tasks_shipped,
|
|
692
692
|
karma: b.karma,
|
|
693
693
|
rank: b.rank || null,
|
|
694
|
-
|
|
694
|
+
// An instance on an older core still reports engineer/ideator (task 1005141).
|
|
695
|
+
disciplines: Array.isArray(b.disciplines) ? api.crafts.normalizeCrafts(b.disciplines) : [],
|
|
695
696
|
recentShips: b.recent_ships,
|
|
696
697
|
crafts: b.crafts,
|
|
697
698
|
// The AUTHENTICATED client's registered origin — the bound every pushed
|
|
@@ -606,14 +606,14 @@ const upsertCall = (pool) => pool.calls.find((c) => /INSERT INTO platform_identi
|
|
|
606
606
|
|
|
607
607
|
await ta('upsertActivity upserts per (client_id, github_id) with the talent snapshot', async () => {
|
|
608
608
|
const pool = activityPool();
|
|
609
|
-
await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['
|
|
609
|
+
await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['artisan'] }, { pool });
|
|
610
610
|
const c = upsertCall(pool);
|
|
611
611
|
assert.match(c.text, /ON CONFLICT \(client_id, github_id\) DO UPDATE/);
|
|
612
612
|
// recent_ships (task 1002803) rides the same row as a jsonb STRING param —
|
|
613
613
|
// an empty list when the sender reported none, never NULL. Its own cap,
|
|
614
614
|
// shape and link bound are covered by tests/hub_recent_ships.mjs. The last param
|
|
615
615
|
// is the report's as-of stamp (task 1004268): null when the sender gave none.
|
|
616
|
-
assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['
|
|
616
|
+
assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['artisan'], '[]', null, '{}']);
|
|
617
617
|
});
|
|
618
618
|
|
|
619
619
|
await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
|
|
@@ -622,7 +622,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
|
|
|
622
622
|
clientId: 'c1', githubId: 42,
|
|
623
623
|
rank: 'Supreme Overlord', // not one of the four core rank keys → stored null
|
|
624
624
|
disciplines: [
|
|
625
|
-
'
|
|
625
|
+
'artisan', 42, null, { evil: true }, // non-strings dropped
|
|
626
626
|
'x'.repeat(33), // over-length dropped
|
|
627
627
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', // valid tail — capped to 8 total
|
|
628
628
|
],
|
|
@@ -633,7 +633,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
|
|
|
633
633
|
assert.ok(disc.length <= 8, `disciplines capped at 8 (got ${disc.length})`);
|
|
634
634
|
assert.ok(disc.every((d) => typeof d === 'string' && d.length > 0 && d.length <= 32),
|
|
635
635
|
'only bounded string elements survive');
|
|
636
|
-
assert.equal(disc[0], '
|
|
636
|
+
assert.equal(disc[0], 'artisan', 'valid elements pass through in order');
|
|
637
637
|
});
|
|
638
638
|
|
|
639
639
|
await ta('upsertActivity keeps a legitimate core rank key', async () => {
|
|
@@ -733,7 +733,7 @@ await ta('getRollupForBuilder guards non-numeric github_id BEFORE any query (no
|
|
|
733
733
|
|
|
734
734
|
await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalogued projects', async () => {
|
|
735
735
|
const pool = mockPool([
|
|
736
|
-
{ rows: [{ projects: 2, credits: 150, works_shipped: 7, karma: 4, last_active: 'T', disciplines: ['
|
|
736
|
+
{ rows: [{ projects: 2, credits: 150, works_shipped: 7, karma: 4, last_active: 'T', disciplines: ['artisan', 'ui'] }] },
|
|
737
737
|
{ rows: [
|
|
738
738
|
{ name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic', is_public: true },
|
|
739
739
|
{ name: 'Secret Proj', origin: 'https://sec.example', rank_held: 'archon', is_public: false },
|
|
@@ -741,7 +741,7 @@ await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalog
|
|
|
741
741
|
]);
|
|
742
742
|
const out = await P.getRollupForBuilder(42, { pool });
|
|
743
743
|
assert.deepEqual(out.totals, { projects: 2, credits: 150, works_shipped: 7, karma: 4 });
|
|
744
|
-
assert.deepEqual(out.disciplines, ['
|
|
744
|
+
assert.deepEqual(out.disciplines, ['artisan', 'ui']);
|
|
745
745
|
assert.equal(out.projects.length, 1, 'only the public catalog row is named');
|
|
746
746
|
// recent_ships rides the project row it belongs to (task 1002803), so a work
|
|
747
747
|
// title can only surface where the project name already may.
|
|
@@ -262,9 +262,8 @@
|
|
|
262
262
|
<div class="commFilters">
|
|
263
263
|
<div class="commTabs" id="craftTabs" role="group" aria-label="Role">
|
|
264
264
|
<button class="commTab" type="button" data-craft="" aria-pressed="true">Everyone</button>
|
|
265
|
-
<button class="commTab" type="button" data-craft="
|
|
265
|
+
<button class="commTab" type="button" data-craft="artisan" aria-pressed="false">Artisans</button>
|
|
266
266
|
<button class="commTab" type="button" data-craft="artist" aria-pressed="false">Artists</button>
|
|
267
|
-
<button class="commTab" type="button" data-craft="ideator" aria-pressed="false">Ideators</button>
|
|
268
267
|
<button class="commTab" type="button" data-craft="ui" aria-pressed="false">Designers</button>
|
|
269
268
|
</div>
|
|
270
269
|
<div class="commWhen">
|
|
@@ -314,9 +313,8 @@
|
|
|
314
313
|
<label class="vh" for="findCraft">Role</label>
|
|
315
314
|
<select id="findCraft">
|
|
316
315
|
<option value="">Any role</option>
|
|
317
|
-
<option value="
|
|
316
|
+
<option value="artisan">Artisans</option>
|
|
318
317
|
<option value="artist">Artists</option>
|
|
319
|
-
<option value="ideator">Ideators</option>
|
|
320
318
|
<option value="ui">Designers</option>
|
|
321
319
|
</select>
|
|
322
320
|
<button id="findBtn" type="submit" class="btn small">Search</button>
|
|
@@ -365,7 +363,7 @@
|
|
|
365
363
|
var WINDOWS = ['7d', '30d', '365d', 'all'];
|
|
366
364
|
var QUERY_MIN = 2;
|
|
367
365
|
var QUERY_MAX = 100;
|
|
368
|
-
var CRAFT_WORD = {
|
|
366
|
+
var CRAFT_WORD = { artisan: 'artisan', artist: 'artist', ui: 'designer' };
|
|
369
367
|
// The five sub-pages this file is served as (platform-server.js's /community routes);
|
|
370
368
|
// the path picks one, and its menu items are marked current. The home adds no title.
|
|
371
369
|
var VIEWS = {
|
|
@@ -774,7 +774,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
774
774
|
<div class="layer">
|
|
775
775
|
<h1 data-brand-tagline><span class="l1">Building Digitally,</span><span class="l2">as simple as Bongos.</span></h1>
|
|
776
776
|
|
|
777
|
-
<p class="lede">Bringing AI to the
|
|
777
|
+
<p class="lede">Bringing AI to the artisan and the artist in everyone.</p>
|
|
778
778
|
|
|
779
779
|
<div class="doors">
|
|
780
780
|
<div class="doorbar">
|
|
@@ -1389,7 +1389,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1389
1389
|
<div class="card mSec" id="mInvite">
|
|
1390
1390
|
<h2>Invite a builder</h2>
|
|
1391
1391
|
<div id="mInv">
|
|
1392
|
-
<p class="finePrint">By GitHub username. The invite is sent from your project’s own hall — that is the invite that lets them in — and they see it on their projects page here too.</p>
|
|
1392
|
+
<p class="finePrint">By GitHub username. The invite is sent from your project’s own hall — that is the invite that lets them in — and they see it on their projects page here too. The hall’s invite box can also invite them to the project’s GitHub repository, which is a separate invite they accept on GitHub.</p>
|
|
1393
1393
|
<div class="attach">
|
|
1394
1394
|
<label class="vh" for="mInviteWho">GitHub username</label>
|
|
1395
1395
|
<input type="text" id="mInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- specialities_004 — specialities follow core_296's craft rename (task 1005141,
|
|
2
|
+
-- BONGOS-V3 C1): engineer → artisan, ideator → artist.
|
|
3
|
+
--
|
|
4
|
+
-- A speciality's discipline is immutable to its OWNER (the update path refuses a
|
|
5
|
+
-- change); this is a vocabulary rename, not a move between crafts, so it rewrites the
|
|
6
|
+
-- catalog and the adoption copy together.
|
|
7
|
+
--
|
|
8
|
+
-- The fold has one collision: specialities_adoptions allows ONE active adoption per
|
|
9
|
+
-- builder per craft. A builder with an active artist speciality AND an active ideator
|
|
10
|
+
-- one would hold two artist adoptions after the rename, so the ideator adoption is
|
|
11
|
+
-- deactivated first (deactivating is how a builder switches; nothing is deleted, and
|
|
12
|
+
-- they can re-adopt it from their speciality page).
|
|
13
|
+
|
|
14
|
+
BEGIN;
|
|
15
|
+
|
|
16
|
+
UPDATE specialities_adoptions i
|
|
17
|
+
SET active = FALSE
|
|
18
|
+
WHERE i.active AND i.discipline = 'ideator'
|
|
19
|
+
AND EXISTS (SELECT 1 FROM specialities_adoptions a
|
|
20
|
+
WHERE a.builder_id = i.builder_id AND a.active AND a.discipline = 'artist');
|
|
21
|
+
|
|
22
|
+
UPDATE specialities_catalog SET discipline = 'artisan' WHERE discipline = 'engineer';
|
|
23
|
+
UPDATE specialities_catalog SET discipline = 'artist' WHERE discipline = 'ideator';
|
|
24
|
+
UPDATE specialities_adoptions SET discipline = 'artisan' WHERE discipline = 'engineer';
|
|
25
|
+
UPDATE specialities_adoptions SET discipline = 'artist' WHERE discipline = 'ideator';
|
|
26
|
+
|
|
27
|
+
INSERT INTO schema_migrations (version) VALUES ('specialities_004_crafts_artisan_artist')
|
|
28
|
+
ON CONFLICT (version) DO NOTHING;
|
|
29
|
+
|
|
30
|
+
COMMIT;
|