@bongos/core 1.24.79 → 1.24.80
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 +495 -350
- 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/README.md +3 -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 +390 -370
- package/docs/copy-registry.json +719 -518
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +2 -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 +15 -4
- package/docs/page-readings.json +3182 -3336
- 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 +4 -4
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- 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 +2 -1
- package/modules/hall-ui/public/city.html +9 -6
- package/modules/hall-ui/public/city.js +7 -1
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- 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/government.html +4 -4
- 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/ideas.html +5 -5
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- 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/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- 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/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 +4 -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/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/shell-game.css +65 -0
- package/modules/hall-ui/public/shell-game.js +283 -3
- package/modules/hall-ui/public/shell.js +7 -26
- 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-inside.js +3 -4
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/style.css +73 -47
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- 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/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/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work.html +5 -5
- 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 +21 -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/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/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +70 -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/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 +26 -9
- package/src/crafts.js +66 -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/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 +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +27 -3
- 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 +2 -2
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +244 -0
- package/tests/hall_street.mjs +2 -2
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- 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
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
// The cross-project rollup used to carry one ship count (with no status filter)
|
|
5
5
|
// and "Roles" from the declared preference alone, so on the hub an artist with
|
|
6
6
|
// forty approved pages, or an ideator the board ratified, read as an engineer
|
|
7
|
-
// with nothing shipped.
|
|
7
|
+
// with nothing shipped. Since task 1005141 (BONGOS-V3) there are two crafts:
|
|
8
|
+
// the engineer is the ARTISAN, and the ideator folded into the ARTIST, whose one
|
|
9
|
+
// block carries the page count AND the idea counts under one credit split. An
|
|
10
|
+
// instance on an older core still reports the old three; the hub folds them.
|
|
11
|
+
// Three halves, each driven through its real code:
|
|
8
12
|
//
|
|
9
13
|
// 1. THE SENDER (core auth-admission.js reportActivityRollup) counts SHIPPED
|
|
10
14
|
// works only, sends each EARNED craft's counts (the ledger split decides,
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
// the body strictly — re-sends once without `crafts` when an older hub
|
|
14
18
|
// refuses the field, so the totals are never lost.
|
|
15
19
|
// 2. THE HUB ROUTE + upsertActivity accept `crafts` and bind only the
|
|
16
|
-
// sanitised shape:
|
|
20
|
+
// sanitised shape: two crafts, named counts, integers or null. Nothing a
|
|
17
21
|
// sender writes can carry text through it.
|
|
18
22
|
// 3. getRollupForBuilder sums each craft over EVERY project row — named or
|
|
19
23
|
// not, exactly as the totals are (the aggregate is not the leak, the name
|
|
@@ -66,12 +70,12 @@ const seams = require('../src/module-seams.js');
|
|
|
66
70
|
// One rollup through the real sender. `byCraft` is economy's ledger split for
|
|
67
71
|
// builder 7 (null = no economy module); `ports` the craft stats; `statuses` the
|
|
68
72
|
// HTTP status each successive send answers.
|
|
69
|
-
async function sendRollup({ byCraft = null, preferred = ['
|
|
73
|
+
async function sendRollup({ byCraft = null, preferred = ['artisan'], ports = {}, statuses = [200] } = {}) {
|
|
70
74
|
seams._reset();
|
|
71
75
|
if (byCraft) {
|
|
72
76
|
seams.registerProvider('reward', {
|
|
73
77
|
creditsByCraft: async (ids) => new Map(ids.map((id) => [String(id), byCraft])),
|
|
74
|
-
craftForPreference: (d) => (d === 'ui' ? 'artist' : ['
|
|
78
|
+
craftForPreference: (d) => (d === 'ui' ? 'artist' : ['artisan', 'artist'].includes(d) ? d : null),
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
for (const [k, fn] of Object.entries(ports)) seams.registerProvider(k, fn);
|
|
@@ -110,52 +114,51 @@ await t('the ship count asks for SHIPPED works only (the missing status filter)'
|
|
|
110
114
|
assert.match(q.text, /status = 'shipped'/, 'a count without the status filter also counts work handed back after shipping');
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
await t('an artist
|
|
117
|
+
await t('an artist with pages AND ideas sends ONE artist block of both halves\' counts, and only counts', async () => {
|
|
114
118
|
const { sent } = await sendRollup({
|
|
115
|
-
byCraft: {
|
|
119
|
+
byCraft: { artisan: 0, artist: 1260, other: 0, total: 1260 },
|
|
116
120
|
preferred: ['ui'],
|
|
117
121
|
ports: { 'copy-desk.artistStats': ARTIST_STATS, 'ideas.ideatorStats': IDEATOR_STATS },
|
|
118
122
|
});
|
|
119
123
|
assert.equal(sent.length, 1);
|
|
120
124
|
const body = sent[0];
|
|
121
125
|
assert.deepEqual(body.crafts, {
|
|
122
|
-
artist: { pages_approved: 42, credits:
|
|
123
|
-
|
|
124
|
-
}, 'exactly the named counts — no surface line, no size, no wording');
|
|
126
|
+
artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 },
|
|
127
|
+
}, 'exactly the named counts — no surface line, no size, no wording; credits are the craft ledger split');
|
|
125
128
|
assert.ok(!JSON.stringify(body).includes('secret wording'), 'no page wording leaves the instance');
|
|
126
|
-
assert.deepEqual(body.disciplines, ['artist'
|
|
129
|
+
assert.deepEqual(body.disciplines, ['artist'], 'crafts from evidence of work, once each (ui → artist)');
|
|
127
130
|
assert.equal(body.tasks_shipped, 5, 'CONTROL: the totals still ride');
|
|
128
131
|
});
|
|
129
132
|
|
|
130
133
|
await t('an unearned craft sends nothing, and its stats port is never asked', async () => {
|
|
131
134
|
let asked = 0;
|
|
132
135
|
const { sent } = await sendRollup({
|
|
133
|
-
byCraft: {
|
|
134
|
-
preferred: ['artist', '
|
|
136
|
+
byCraft: { artisan: 900, artist: 0, other: 0, total: 900 },
|
|
137
|
+
preferred: ['artist', 'artisan'],
|
|
135
138
|
ports: { 'copy-desk.artistStats': async () => { asked++; return {}; } },
|
|
136
139
|
});
|
|
137
|
-
assert.deepEqual(sent[0].crafts, {
|
|
138
|
-
assert.deepEqual(sent[0].disciplines, ['
|
|
140
|
+
assert.deepEqual(sent[0].crafts, { artisan: { shipped: 5, credits: 900 } });
|
|
141
|
+
assert.deepEqual(sent[0].disciplines, ['artisan'], 'a declared but unearned role is not shown once work exists elsewhere');
|
|
139
142
|
assert.equal(asked, 0, 'a craft that will not be sent is not read');
|
|
140
143
|
});
|
|
141
144
|
|
|
142
145
|
await t('nothing earned, or no economy to ask → the declared preference, and no crafts field', async () => {
|
|
143
|
-
const none = await sendRollup({ byCraft: {
|
|
144
|
-
assert.deepEqual(none.sent[0].disciplines, ['
|
|
146
|
+
const none = await sendRollup({ byCraft: { artisan: 0, artist: 0, other: 0, total: 0 }, preferred: ['artist'] });
|
|
147
|
+
assert.deepEqual(none.sent[0].disciplines, ['artist'], 'the fallback is the preference');
|
|
145
148
|
assert.ok(!('crafts' in none.sent[0]), 'no crafts field, so an older hub never sees one');
|
|
146
|
-
const vanilla = await sendRollup({ byCraft: null, preferred: ['
|
|
147
|
-
assert.deepEqual(vanilla.sent[0].disciplines, ['
|
|
149
|
+
const vanilla = await sendRollup({ byCraft: null, preferred: ['artisan', 'ui'] });
|
|
150
|
+
assert.deepEqual(vanilla.sent[0].disciplines, ['artisan', 'ui']);
|
|
148
151
|
assert.ok(!('crafts' in vanilla.sent[0]));
|
|
149
152
|
});
|
|
150
153
|
|
|
151
154
|
await t('a craft whose port cannot be asked sends null counts, never 0', async () => {
|
|
152
|
-
const { sent } = await sendRollup({ byCraft: {
|
|
153
|
-
assert.deepEqual(sent[0].crafts, { artist: { pages_approved: null, credits: 50 } });
|
|
155
|
+
const { sent } = await sendRollup({ byCraft: { artisan: 0, artist: 50, other: 0, total: 50 }, preferred: [] });
|
|
156
|
+
assert.deepEqual(sent[0].crafts, { artist: { pages_approved: null, filed: null, full_ideas: null, ratified: null, credits: 50 } });
|
|
154
157
|
});
|
|
155
158
|
|
|
156
159
|
await t('an older hub refusing `crafts` (400) gets the report again without it — once', async () => {
|
|
157
160
|
const { sent } = await sendRollup({
|
|
158
|
-
byCraft: {
|
|
161
|
+
byCraft: { artisan: 0, artist: 1100, other: 0, total: 1100 },
|
|
159
162
|
ports: { 'copy-desk.artistStats': ARTIST_STATS },
|
|
160
163
|
statuses: [400, 200],
|
|
161
164
|
});
|
|
@@ -195,6 +198,7 @@ function hubRoute() {
|
|
|
195
198
|
resolveSession: async () => null,
|
|
196
199
|
logger: () => ({ info() {}, warn() {}, error() {} }),
|
|
197
200
|
LIVE_RANK_LADDER: realApi.LIVE_RANK_LADDER,
|
|
201
|
+
crafts: realApi.crafts,
|
|
198
202
|
withTx: realApi.withTx,
|
|
199
203
|
corsPublicGet: () => (_q, _s, next) => next(),
|
|
200
204
|
on: () => () => {},
|
|
@@ -225,26 +229,60 @@ await t('a report carrying crafts is accepted and binds the craft counts', async
|
|
|
225
229
|
assert.equal(res.failure, null, JSON.stringify(res.failure));
|
|
226
230
|
assert.match(insert.text, /reported_as_of, crafts\)/, 'the upsert writes the crafts column');
|
|
227
231
|
assert.match(insert.text, /crafts = EXCLUDED\.crafts/, 'and a later report replaces it');
|
|
228
|
-
assert.deepEqual(JSON.parse(insert.params[9]), {
|
|
232
|
+
assert.deepEqual(JSON.parse(insert.params[9]), {
|
|
233
|
+
artist: { pages_approved: 42, filed: null, full_ideas: null, ratified: null, credits: 1100 },
|
|
234
|
+
}, 'a count the report did not carry is bound as null, never 0');
|
|
229
235
|
});
|
|
230
236
|
|
|
231
|
-
await t('
|
|
237
|
+
await t('a report from an older core (engineer / artist / ideator) is folded into today\'s two crafts', async () => {
|
|
238
|
+
const { res, insert } = await postRollup({
|
|
239
|
+
crafts: {
|
|
240
|
+
engineer: { shipped: 3, credits: 9 },
|
|
241
|
+
artist: { pages_approved: 42, credits: 1100 },
|
|
242
|
+
ideator: { filed: 12, full_ideas: 2, ratified: 1, credits: 160 },
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
assert.equal(res.failure, null, JSON.stringify(res.failure));
|
|
246
|
+
assert.deepEqual(JSON.parse(insert.params[9]), {
|
|
247
|
+
artisan: { shipped: 3, credits: 9 },
|
|
248
|
+
artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 },
|
|
249
|
+
}, 'engineer → artisan; the ideator counts join the artist block, credits summed');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await t('the hub keeps only the two crafts and their named integer counts', async () => {
|
|
232
253
|
const { insert } = await postRollup({
|
|
233
254
|
crafts: {
|
|
234
|
-
artist: { pages_approved: 'forty', credits: -3, title: '<script>' },
|
|
235
|
-
ideator: { filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: 7, idea_title: 'A secret plan' },
|
|
255
|
+
artist: { pages_approved: 'forty', filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: -3, title: '<script>', idea_title: 'A secret plan' },
|
|
236
256
|
governor: { laws: 4 },
|
|
237
|
-
|
|
257
|
+
artisan: 'lots',
|
|
238
258
|
},
|
|
239
259
|
});
|
|
240
260
|
const bound = JSON.parse(insert.params[9]);
|
|
241
261
|
assert.deepEqual(bound, {
|
|
242
|
-
artist: { pages_approved: null, credits: null },
|
|
243
|
-
ideator: { filed: null, full_ideas: null, ratified: 2, credits: 7 },
|
|
262
|
+
artist: { pages_approved: null, filed: null, full_ideas: null, ratified: 2, credits: null },
|
|
244
263
|
});
|
|
245
264
|
assert.ok(!insert.params[9].includes('secret') && !insert.params[9].includes('script'), 'no text survives');
|
|
246
265
|
});
|
|
247
266
|
|
|
267
|
+
await t('an old-shape report is capped half by half: a hostile count on one side cannot shave the other', async () => {
|
|
268
|
+
const { insert } = await postRollup({
|
|
269
|
+
crafts: {
|
|
270
|
+
artist: { pages_approved: 'forty', credits: -3, title: '<script>' },
|
|
271
|
+
ideator: { filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: 7, idea_title: 'A secret plan' },
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
assert.deepEqual(JSON.parse(insert.params[9]), {
|
|
275
|
+
artist: { pages_approved: null, filed: null, full_ideas: null, ratified: 2, credits: 7 },
|
|
276
|
+
}, 'the -3 is dropped before the sum, never subtracted from the 7');
|
|
277
|
+
assert.ok(!insert.params[9].includes('secret') && !insert.params[9].includes('script'), 'no text survives the fold');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await t('an older core\'s disciplines (engineer, ideator) are read as today\'s crafts, once each', async () => {
|
|
281
|
+
const { res, insert } = await postRollup({ disciplines: ['engineer', 'ideator', 'artist', 'ui'] });
|
|
282
|
+
assert.equal(res.failure, null, JSON.stringify(res.failure));
|
|
283
|
+
assert.deepEqual(insert.params[6], ['artisan', 'artist', 'ui'], 'engineer → artisan; ideator and artist are one craft; ui stays');
|
|
284
|
+
});
|
|
285
|
+
|
|
248
286
|
await t('a report with no crafts (an older instance) binds the empty object', async () => {
|
|
249
287
|
const { res, insert } = await postRollup({});
|
|
250
288
|
assert.deepEqual(res.body, { ok: true });
|
|
@@ -271,6 +309,7 @@ await t('getRollupForBuilder sums each craft over EVERY project row, named or no
|
|
|
271
309
|
return { rows: [
|
|
272
310
|
{ name: 'Pub', origin: 'https://pub.example', rank_held: 'metic', recent_ships: [], is_public: true,
|
|
273
311
|
crafts: { artist: { pages_approved: 40, credits: 1000 } } },
|
|
312
|
+
// A row stored by an older core, before the hub migration ran: folded on read.
|
|
274
313
|
{ name: 'Hidden', origin: 'https://priv.example', rank_held: 'xenos', recent_ships: [], is_public: false,
|
|
275
314
|
crafts: { artist: { pages_approved: 2, credits: null }, ideator: { filed: 5, full_ideas: 1, ratified: null, credits: 60 } } },
|
|
276
315
|
{ name: 'Old', origin: 'https://old.example', rank_held: null, recent_ships: [], is_public: true, crafts: {} },
|
|
@@ -281,9 +320,8 @@ await t('getRollupForBuilder sums each craft over EVERY project row, named or no
|
|
|
281
320
|
const r = await pi.getRollupForBuilder(42, { pool });
|
|
282
321
|
assert.match(pool.find(/recent_ships/).text, /a\.crafts/, 'the per-project read carries crafts');
|
|
283
322
|
assert.deepEqual(r.crafts, {
|
|
284
|
-
artist: { pages_approved: 42, credits:
|
|
285
|
-
|
|
286
|
-
}, 'a private project is counted, as its totals are; a count nobody reported stays null');
|
|
323
|
+
artist: { pages_approved: 42, filed: 5, full_ideas: 1, ratified: null, credits: 1060 },
|
|
324
|
+
}, 'a private project is counted, as its totals are; an old-shape row folds in; a count nobody reported stays null');
|
|
287
325
|
assert.deepEqual(r.projects.map((p) => p.name), ['Pub', 'Old'], 'CONTROL: the private project is still not NAMED');
|
|
288
326
|
assert.ok(!JSON.stringify(r.projects).includes('pages_approved'), 'per-project numbers never cross, only the sum');
|
|
289
327
|
});
|
|
@@ -323,8 +361,8 @@ const tileOf = (html, label) => {
|
|
|
323
361
|
return m ? m[1] : null;
|
|
324
362
|
};
|
|
325
363
|
|
|
326
|
-
await t('
|
|
327
|
-
const html = await renderXp(CP({ artist: { pages_approved: 42,
|
|
364
|
+
await t('the artist\'s tiles show both halves of the craft: pages, thoughts and Full Ideas', async () => {
|
|
365
|
+
const html = await renderXp(CP({ artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 } }));
|
|
328
366
|
assert.equal(tileOf(html, 'Pages approved'), '42');
|
|
329
367
|
assert.equal(tileOf(html, 'Thoughts filed'), '12');
|
|
330
368
|
assert.equal(tileOf(html, 'Full Ideas'), '2');
|
|
@@ -334,12 +372,13 @@ await t('an artist\'s and an ideator\'s tiles show their own craft\'s numbers',
|
|
|
334
372
|
});
|
|
335
373
|
|
|
336
374
|
await t('a count no project could report is a dash, never 0', async () => {
|
|
337
|
-
const html = await renderXp(CP({ artist: { pages_approved: null, credits: 50 } }));
|
|
375
|
+
const html = await renderXp(CP({ artist: { pages_approved: null, filed: null, full_ideas: null, ratified: null, credits: 50 } }));
|
|
338
376
|
assert.equal(tileOf(html, 'Pages approved'), '—');
|
|
377
|
+
assert.equal(tileOf(html, 'Thoughts filed'), '—');
|
|
339
378
|
});
|
|
340
379
|
|
|
341
|
-
await t('no crafts (an
|
|
342
|
-
for (const crafts of [undefined, {}, {
|
|
380
|
+
await t('no crafts (an artisan, or an older hub) → no craft band at all', async () => {
|
|
381
|
+
for (const crafts of [undefined, {}, { artisan: { shipped: 3, credits: 9 } }]) {
|
|
343
382
|
const html = await renderXp(CP(crafts));
|
|
344
383
|
assert.ok(!html.includes('stat-row--craft'), `no craft band for ${JSON.stringify(crafts)}`);
|
|
345
384
|
assert.equal((html.match(/stat-tile__num/g) || []).length, 4);
|
|
@@ -184,6 +184,7 @@ await t('the route passes recent_ships through and takes the origin from the CLI
|
|
|
184
184
|
resolveSession: async () => null,
|
|
185
185
|
logger: () => ({ info() {}, warn() {}, error() {} }),
|
|
186
186
|
LIVE_RANK_LADDER: realApi.LIVE_RANK_LADDER,
|
|
187
|
+
crafts: realApi.crafts, // the rollup normalizes reported disciplines (task 1005141)
|
|
187
188
|
corsPublicGet: () => (_q, _s, next) => next(),
|
|
188
189
|
on: () => () => {},
|
|
189
190
|
hasProvider: () => false,
|
|
@@ -201,7 +202,7 @@ await t('the route passes recent_ships through and takes the origin from the CLI
|
|
|
201
202
|
const req = {
|
|
202
203
|
body: {
|
|
203
204
|
client_id: 'c1', client_secret: SECRET, github_id: 42,
|
|
204
|
-
credits: 10, tasks_shipped: 2, karma: 1, rank: 'metic', disciplines: ['
|
|
205
|
+
credits: 10, tasks_shipped: 2, karma: 1, rank: 'metic', disciplines: ['artisan'],
|
|
205
206
|
// Five, so the HUB's cap is what decides — a sender that ignores its own
|
|
206
207
|
// cap must not widen the section.
|
|
207
208
|
recent_ships: [1, 2, 3, 4, 5].map((n) => ship({ id: n, title: `work ${n}`, url: `${ORIGIN}/builders#/task/${n}` })),
|
|
@@ -222,7 +223,7 @@ console.log('\n4. a work title surfaces only where the project name may:');
|
|
|
222
223
|
await t('getRollupForBuilder attaches recent_ships to publicly-named projects only', async () => {
|
|
223
224
|
const pool = mockPool((sql) => {
|
|
224
225
|
if (/WITH agg AS/.test(sql)) {
|
|
225
|
-
return { rows: [{ projects: 2, credits: 5, works_shipped: 2, karma: 0, last_active: null, disciplines: ['
|
|
226
|
+
return { rows: [{ projects: 2, credits: 5, works_shipped: 2, karma: 0, last_active: null, disciplines: ['artisan'] }] };
|
|
226
227
|
}
|
|
227
228
|
return {
|
|
228
229
|
rows: [
|
package/tests/idea_cascade.mjs
CHANGED
|
@@ -51,7 +51,7 @@ test('a Full Idea matches, and files for the ENGINEER', () => {
|
|
|
51
51
|
const { matched, errors } = C.cascadesFor(idea(), C.IDEA_CASCADES, {}, C.TRIGGERS.idea);
|
|
52
52
|
assert.deepEqual(errors, []);
|
|
53
53
|
assert.equal(matched.length, 1);
|
|
54
|
-
assert.equal(matched[0].rule.discipline, '
|
|
54
|
+
assert.equal(matched[0].rule.discipline, 'artisan');
|
|
55
55
|
assert.match(matched[0].spec.title, /Scope the Full Idea: Price swaps from mechanism/);
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -138,7 +138,7 @@ test('the runner files the task from the event payload alone', async () => {
|
|
|
138
138
|
assert.deepEqual(report.errors, []);
|
|
139
139
|
assert.equal(report.created.length, 1);
|
|
140
140
|
assert.equal(d.created.length, 1);
|
|
141
|
-
assert.equal(d.created[0].discipline, '
|
|
141
|
+
assert.equal(d.created[0].discipline, 'artisan');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
test('an already-filed cascade is SKIPPED, not filed twice', async () => {
|
|
@@ -147,7 +147,7 @@ async function fixture(client, { completeness = 70, developed = false, taskCount
|
|
|
147
147
|
const task = await insertRow(client, 'tasks', {
|
|
148
148
|
// discipline is named for the same reason as the version status above:
|
|
149
149
|
// tasks_discipline_check admits only the live crafts.
|
|
150
|
-
version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: '
|
|
150
|
+
version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: 'artisan',
|
|
151
151
|
});
|
|
152
152
|
await insertRow(client, 'ideas_task_lineage', {
|
|
153
153
|
task_id: task.id, idea_id: idea.id, origin: 'promote',
|
|
@@ -95,7 +95,7 @@ function render(idea, { template = TEMPLATE, templateFails = false, ideaStatus =
|
|
|
95
95
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
96
96
|
roleAxisLabel: () => 'Role',
|
|
97
97
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
98
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
98
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
99
99
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
100
100
|
ROLE_UNSET: 'unclassified',
|
|
101
101
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// WHERE THAT PLAYBOOK LIVES (task 1002990). It used to be the /ideate skill. The
|
|
7
7
|
// kernel/pack split moved it into the Ideator role pack and deleted the skill, so
|
|
8
|
-
// this suite now reads docs/packs/
|
|
8
|
+
// this suite now reads docs/packs/artist.md. Every contract below is unchanged —
|
|
9
9
|
// the file moved, the promises did not — and pointing the suite at the new home is
|
|
10
10
|
// what stops the split from silently dropping the guard with the skill.
|
|
11
11
|
//
|
|
@@ -37,7 +37,10 @@ import { namesFieldKey } from './helpers.mjs';
|
|
|
37
37
|
|
|
38
38
|
const require = createRequire(import.meta.url);
|
|
39
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
|
-
|
|
40
|
+
// The ideas half's detail moved to docs/recipes/ideation-playbook.md when the ideator
|
|
41
|
+
// folded into the artist (task 1005141): the pack and its playbook are read as one text.
|
|
42
|
+
const SKILL = fs.readFileSync(path.join(ROOT, 'docs/packs/artist.md'), 'utf8')
|
|
43
|
+
+ '\n' + fs.readFileSync(path.join(ROOT, 'docs/recipes/ideation-playbook.md'), 'utf8');
|
|
41
44
|
// Prose assertions run against a WHITESPACE-NORMALISED copy. Markdown is hard-
|
|
42
45
|
// wrapped, so any phrase long enough to be worth asserting will eventually
|
|
43
46
|
// straddle a newline — and a regex that does not allow for that fails on a
|
|
@@ -195,7 +195,7 @@ function boot({
|
|
|
195
195
|
// the same reason categoryChip is: an undefined helper throws mid-render.
|
|
196
196
|
roleAxisLabel: () => 'Role',
|
|
197
197
|
roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
|
|
198
|
-
roleOptions: () => [{ value: '', label: '— any —' }, { value: '
|
|
198
|
+
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
|
|
199
199
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
200
200
|
ROLE_UNSET: 'unclassified',
|
|
201
201
|
escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])),
|
package/tests/init.mjs
CHANGED
|
@@ -134,7 +134,7 @@ test('kickoffTasks: includes the load-bearing owner-manual steps', () => {
|
|
|
134
134
|
|
|
135
135
|
// Every kickoff task must satisfy the DB CHECK constraints (tasks_kind_check /
|
|
136
136
|
// tasks_discipline_check) or the insert is rejected — which is how the branding
|
|
137
|
-
// step vanished on 1.18.18: kind:'
|
|
137
|
+
// step vanished on 1.18.18: kind:'artist' (a discipline, not a kind) violated
|
|
138
138
|
// tasks_kind_check and postTask absorbed the failure. Pin the vocabulary at the
|
|
139
139
|
// source so this class of bug can't reach a user again.
|
|
140
140
|
test('kickoffTasks: every kind/discipline satisfies the DB CHECK vocabulary', () => {
|
|
@@ -156,7 +156,7 @@ test('kickoffTasks: the branding step is a decision BY an ideator (kind vs disci
|
|
|
156
156
|
const look = init.kickoffTasks(fullSpec()).find((t) => /instance look/i.test(t.title));
|
|
157
157
|
assert.ok(look, 'the branding kickoff step exists');
|
|
158
158
|
assert.equal(look.kind, 'decision');
|
|
159
|
-
assert.equal(look.discipline, '
|
|
159
|
+
assert.equal(look.discipline, 'artist');
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
// ---- validateSpec ----
|
|
@@ -695,7 +695,7 @@ async function runSeedTests() {
|
|
|
695
695
|
const look = findTaskPost(api, /instance look/i);
|
|
696
696
|
assert.ok(look, 'the branding kickoff task was POSTed');
|
|
697
697
|
assert.equal(look.body.kind, 'decision');
|
|
698
|
-
assert.equal(look.body.discipline, '
|
|
698
|
+
assert.equal(look.body.discipline, 'artist');
|
|
699
699
|
});
|
|
700
700
|
|
|
701
701
|
await at('seedViaApi: a rejected task POST is LOUD — failures returned, counts reflect actual vs expected', async () => {
|
|
@@ -31,11 +31,11 @@ const BRAND_VANILLA = {
|
|
|
31
31
|
};
|
|
32
32
|
const MODULES_OTB = {
|
|
33
33
|
enabled: { game: true, 'art-pipeline': true, discord: true },
|
|
34
|
-
disciplines: ['
|
|
34
|
+
disciplines: ['artisan', 'artist'],
|
|
35
35
|
};
|
|
36
36
|
const MODULES_VANILLA = {
|
|
37
37
|
enabled: { game: false, 'art-pipeline': false, discord: false },
|
|
38
|
-
disciplines: ['
|
|
38
|
+
disciplines: ['artisan'],
|
|
39
39
|
};
|
|
40
40
|
const REPO = { owner: 'example-owner', repo: 'example', branch: 'main', blob_base: 'x', tree_base: 'y' };
|
|
41
41
|
|
|
@@ -96,7 +96,7 @@ test('the look step is a decision BY an ideator in every variant', () => {
|
|
|
96
96
|
const look = kickoffChecklist(spec(), opts).find((t) => /instance look/i.test(t.title));
|
|
97
97
|
assert.ok(look, `look step present for ${JSON.stringify(opts)}`);
|
|
98
98
|
assert.equal(look.kind, 'decision');
|
|
99
|
-
assert.equal(look.discipline, '
|
|
99
|
+
assert.equal(look.discipline, 'artist');
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// credits (the figure a role ranks by is in the row, ADR 0255), and a roles
|
|
13
13
|
// failure costs the roles, never the board.
|
|
14
14
|
// 3. THE ROSTER. The REAL roster.js over the harness's public fixture: a Roles
|
|
15
|
-
// column, the Everyone /
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// column, the Everyone / Artisans / Artists toggle ranking by the credits
|
|
16
|
+
// that craft earned, and a dash, not a 0, for a builder with no AI sessions.
|
|
17
|
+
// (Two crafts since task 1005141: the engineer is the artisan, and the
|
|
18
|
+
// ideator folded into the artist, so idea credits rank as artist credits.)
|
|
18
19
|
// 4. THE STANDING CARD and 5. THE STATUS PAGE: the real functions, run.
|
|
19
20
|
|
|
20
21
|
import { strict as assert } from 'node:assert';
|
|
@@ -49,26 +50,27 @@ await test('a page of builders: chosen, earned and preferred main roles, and a c
|
|
|
49
50
|
const calls = [];
|
|
50
51
|
seams._reset();
|
|
51
52
|
seams.registerProvider('reward', rewardOver([
|
|
52
|
-
{ builder_id: '1', discipline: '
|
|
53
|
-
{ builder_id: '1', discipline: '
|
|
54
|
-
|
|
55
|
-
{ builder_id: '2', discipline: '
|
|
56
|
-
{ builder_id: '3', discipline: '
|
|
53
|
+
{ builder_id: '1', discipline: 'artisan', amount: 900 },
|
|
54
|
+
{ builder_id: '1', discipline: 'artist', amount: 20 },
|
|
55
|
+
// pages (370) and ideas (42): one craft, so the grouped statement sends one row
|
|
56
|
+
{ builder_id: '2', discipline: 'artist', amount: 412 },
|
|
57
|
+
{ builder_id: '3', discipline: 'artist', amount: 186 },
|
|
57
58
|
], calls));
|
|
58
59
|
const choices = new Map([
|
|
59
|
-
['2', { preferred_disciplines: ['artist'
|
|
60
|
-
['3', { preferred_disciplines: ['
|
|
60
|
+
['2', { preferred_disciplines: ['artist'], main_discipline: 'artist' }],
|
|
61
|
+
['3', { preferred_disciplines: ['artisan', 'artist'], main_discipline: null }],
|
|
61
62
|
['4', { preferred_disciplines: ['ui'], main_discipline: null }],
|
|
62
63
|
]);
|
|
63
64
|
const out = await readRolesForBuilders(['1', '2', 3, '4', 'x', '2'], { getRoleChoices: async (ids) => { calls.push(['choices', ids]); return choices; } });
|
|
64
65
|
assert.deepEqual([...out.keys()], ['1', '2', '3', '4'], 'every valid id once, a bad one dropped');
|
|
65
66
|
const r = (id) => JSON.parse(JSON.stringify(out.get(id)));
|
|
66
|
-
assert.deepEqual([r('1').main, r('1').main_basis, r('1').shown], ['
|
|
67
|
-
assert.deepEqual([r('2').main, r('2').main_basis, r('2').shown], ['artist', 'chosen', ['artist', '
|
|
68
|
-
assert.deepEqual(r('3').shown, ['
|
|
67
|
+
assert.deepEqual([r('1').main, r('1').main_basis, r('1').shown], ['artisan', 'earned', ['artisan', 'artist']]);
|
|
68
|
+
assert.deepEqual([r('2').main, r('2').main_basis, r('2').shown], ['artist', 'chosen', ['artist']], 'one artist, never two');
|
|
69
|
+
assert.deepEqual([r('3').main, r('3').main_basis, r('3').shown], ['artist', 'earned', ['artist']], 'a preferred craft that earned nothing does not show');
|
|
69
70
|
assert.deepEqual([r('4').main, r('4').main_basis, r('4').shown], ['artist', 'preferred', ['artist']], 'ui is the artist\'s craft, as on the profile');
|
|
70
|
-
assert.equal(r('2').credits_by_craft.artist,
|
|
71
|
+
assert.equal(r('2').credits_by_craft.artist, 412, 'pages and ideas, one craft');
|
|
71
72
|
assert.equal(r('2').credits_by_craft.total, 412);
|
|
73
|
+
assert.equal(r('2').credits_by_craft.ideator, undefined, 'no ideator column any more');
|
|
72
74
|
assert.deepEqual(calls.map((c) => c[0]).sort(), ['choices', 'credits'], 'two reads for the whole page, never one per row');
|
|
73
75
|
seams._reset();
|
|
74
76
|
});
|
|
@@ -90,7 +92,7 @@ async function bootPublic({ rolesThrow = false } = {}) {
|
|
|
90
92
|
{ id: '2001', github_login: 'art', total_credits: 370, tokens_used: '0' },
|
|
91
93
|
],
|
|
92
94
|
creditsByCraft: async (ids) => creditsByCraftLib.foldCraftRows(ids, [
|
|
93
|
-
{ builder_id: '1', discipline: '
|
|
95
|
+
{ builder_id: '1', discipline: 'artisan', amount: 900 },
|
|
94
96
|
{ builder_id: '2001', discipline: 'artist', amount: 370 },
|
|
95
97
|
]),
|
|
96
98
|
});
|
|
@@ -117,7 +119,7 @@ await test('GET /public/leaderboard: every row names its roles and the credits e
|
|
|
117
119
|
const { status, body } = await s.get('/public/leaderboard');
|
|
118
120
|
assert.equal(status, 200);
|
|
119
121
|
const [eng, art] = body.leaderboard;
|
|
120
|
-
assert.deepEqual(eng.roles.shown, ['
|
|
122
|
+
assert.deepEqual(eng.roles.shown, ['artisan']);
|
|
121
123
|
assert.deepEqual(art.roles.shown, ['artist']);
|
|
122
124
|
assert.equal(art.roles.main_basis, 'chosen');
|
|
123
125
|
assert.equal(art.roles.credits_by_craft.artist, 370, 'the figure an Artists toggle ranks by is in the row');
|
|
@@ -190,8 +192,11 @@ await test('the roster reads the public list and draws a Roles column, main role
|
|
|
190
192
|
assert.deepEqual(labels, ['Place', 'Builder', 'Roles', 'Rank', 'Credits', 'Karma', 'Tokens']);
|
|
191
193
|
const art = r.rows.find((b) => String(b.id) === '2001');
|
|
192
194
|
const html = r.opts.rowHtml(art);
|
|
193
|
-
assert.match(html, /<span class="lb-role lb-role--lead">Artist<\/span
|
|
194
|
-
|
|
195
|
+
assert.match(html, /<td class="ledger__roles"><span class="lb-roles"><span class="lb-role lb-role--lead">Artist<\/span><\/span><\/td>/, 'one Artist chip, and no Ideator beside it');
|
|
196
|
+
const mixed = r.rows.find((b) => String(b.id) === '3');
|
|
197
|
+
assert.match(r.opts.rowHtml(mixed), /<span class="lb-role lb-role--lead">Artist<\/span><span class="lb-role">Artisan<\/span>/, 'main role first');
|
|
198
|
+
assert.match(r.els['roster-roles'].innerHTML, /Everyone[\s\S]*Artisans[\s\S]*Artists/);
|
|
199
|
+
assert.doesNotMatch(r.els['roster-roles'].innerHTML, /Ideators|Engineers/);
|
|
195
200
|
});
|
|
196
201
|
|
|
197
202
|
await test('a builder with no AI sessions reads a dash and a sentence, never a 0 of tokens', async () => {
|
|
@@ -216,11 +221,15 @@ await test('the Artists toggle shows only artists, ranked by the credits that cr
|
|
|
216
221
|
assert.deepEqual(ids.sort(), artists.map((b) => String(b.id)).sort(), 'exactly the builders shown as artists');
|
|
217
222
|
const credits = r.rows.map((b) => b.roles.credits_by_craft.artist);
|
|
218
223
|
assert.deepEqual(credits, [...credits].sort((a, b) => b - a), 'ranked by artist credits, not total credits');
|
|
219
|
-
|
|
224
|
+
// The top artisan's idea credits (1,159) are artist credits now, so they lead
|
|
225
|
+
// the artists; the browser-only artist is next, by her own 412, never a 0.
|
|
226
|
+
assert.deepEqual(r.rows.slice(0, 2).map((b) => String(b.id)), ['1', '2001'], 'ranked by artist credits, idea credits included');
|
|
220
227
|
const labels = r.opts.columns.map((c) => (typeof c === 'string' ? c : c.label));
|
|
221
228
|
assert.equal(labels[4], 'Credits as artist', 'the header names the craft');
|
|
222
229
|
assert.match(r.opts.rowHtml(r.rows[0]), /ledger__place">1</, 'the place is the place among artists');
|
|
223
|
-
assert.match(r.opts.rowHtml(r.rows[0]), /<td class="ledger__num">
|
|
230
|
+
assert.match(r.opts.rowHtml(r.rows[0]), /<td class="ledger__num">1159<\/td>/, 'and the figure is the artist credits, not the 38,644 total');
|
|
231
|
+
assert.match(r.opts.rowHtml(r.rows[1]), /ledger__place">2</);
|
|
232
|
+
assert.match(r.opts.rowHtml(r.rows[1]), /<td class="ledger__num">412<\/td>/, 'the browser-only artist reads her artist credits');
|
|
224
233
|
r.click('');
|
|
225
234
|
assert.equal(r.rows.length, FIX.leaderboard.length, 'Everyone brings every builder back');
|
|
226
235
|
});
|
|
@@ -242,9 +251,9 @@ await test('the standing card adds the place inside the builder\'s main role, on
|
|
|
242
251
|
vm.runInContext(`${code}; lbState.rows = rows; this.place = leaderboardPlace; this.text = rolePlaceText;`, sandbox);
|
|
243
252
|
const art = sandbox.place('builder-7');
|
|
244
253
|
assert.equal(art.role.craft, 'artist');
|
|
245
|
-
assert.equal(sandbox.text(art), `#
|
|
254
|
+
assert.equal(sandbox.text(art), `#2 as an artist`, 'fourth overall, second among artists: something new, so it is said');
|
|
246
255
|
const top = sandbox.place(FIX.leaderboard[0].github_login);
|
|
247
|
-
assert.equal(sandbox.text(top), '', 'the top
|
|
256
|
+
assert.equal(sandbox.text(top), '', 'the top artisan is #1 either way, so nothing is added');
|
|
248
257
|
// task 1005054: the stats strip (and its rank tile) left Home; the Standing chip's card carries the role place
|
|
249
258
|
assert.match(src, /\$\{rolePlaceText\(rollPlace\) \? `<span title="Your place among the/);
|
|
250
259
|
assert.match(read('modules/hall-ui/public/builders.js'), /getJSON\(`\$\{API\}\/public\/leaderboard`\)/, 'the hall reads the list with the roles');
|
|
@@ -264,11 +273,11 @@ await test('the status leaderboard names each builder\'s roles, main one first,
|
|
|
264
273
|
const sandbox = { document: { createElement: node } };
|
|
265
274
|
vm.createContext(sandbox);
|
|
266
275
|
vm.runInContext(`${code}; this.renderRoles = renderRoles;`, sandbox);
|
|
267
|
-
const row = sandbox.renderRoles({ shown: ['artist', 'ideator', 'wizard'] });
|
|
276
|
+
const row = sandbox.renderRoles({ shown: ['artist', 'artisan', 'ideator', 'wizard'] });
|
|
268
277
|
assert.deepEqual(row.children.map((c) => [c.className, c.textContent]), [
|
|
269
278
|
['leader__role leader__role--lead', 'Artist'],
|
|
270
|
-
['leader__role', '
|
|
271
|
-
], 'a word the page does not know is dropped, not printed');
|
|
279
|
+
['leader__role', 'Artisan'],
|
|
280
|
+
], 'a word the page does not know (the retired ideator included) is dropped, not printed');
|
|
272
281
|
assert.equal(sandbox.renderRoles(null), null, 'a row without roles draws nothing');
|
|
273
282
|
assert.match(src, /const roles = renderRoles\(b\.roles\);\s*if \(roles\) name\.appendChild\(roles\);/);
|
|
274
283
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// tests/lifecycle_claim_ladder_refs.mjs — the claim refusal ladder in
|
|
2
|
+
// modules/lifecycle/CLAUDE.md points at code by NAME, never by line number.
|
|
3
|
+
//
|
|
4
|
+
// task 1005183: the ladder cited `claims.js:138`, `:176`, `db-claims.js:414` and a dozen
|
|
5
|
+
// more bare `:NNN` anchors; every one had drifted (gateOneActiveClaimPerSession was at
|
|
6
|
+
// :208, claimTasksBatch at :541). A line number rots on the next edit above it and then
|
|
7
|
+
// sends a reader to the wrong code with full confidence. A function name or a refusal
|
|
8
|
+
// code is greppable and fails loudly here when it is renamed away.
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
const doc = readFileSync(new URL('../modules/lifecycle/CLAUDE.md', import.meta.url), 'utf8');
|
|
17
|
+
const dbClaims = readFileSync(new URL('../modules/lifecycle/db-claims.js', import.meta.url), 'utf8');
|
|
18
|
+
const routes = readFileSync(new URL('../modules/lifecycle/routes/claims.js', import.meta.url), 'utf8');
|
|
19
|
+
|
|
20
|
+
function ladderSection() {
|
|
21
|
+
const start = doc.indexOf('## The claim refusal ladder');
|
|
22
|
+
assert.ok(start >= 0, 'the ladder section exists');
|
|
23
|
+
const next = doc.indexOf('\n## ', start + 5);
|
|
24
|
+
return doc.slice(start, next < 0 ? undefined : next);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('the claim ladder carries no line-number anchors (task 1005183)', () => {
|
|
28
|
+
const s = ladderSection();
|
|
29
|
+
const stale = s.match(/`[\w./-]*:\d+`|`[\w./-]+\.js:\d+`|\b[\w-]+\.js:\d+\b/g) || [];
|
|
30
|
+
assert.deepEqual(stale, [], `line-number anchors rot — name the function or refusal code instead: ${stale.join(', ')}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('every function the ladder names exists where it says', () => {
|
|
34
|
+
// The db-claims entry points are loaded, not grepped: a rename or a dropped export
|
|
35
|
+
// reds this even when the old wording survives in a comment.
|
|
36
|
+
const claims = require('../modules/lifecycle/db-claims.js');
|
|
37
|
+
for (const fn of ['claimTask', 'claimTasksBatch', 'validateClaimBatch', 'claimUniqueViolationError']) {
|
|
38
|
+
assert.equal(typeof claims[fn], 'function', `db-claims.js exports ${fn}`);
|
|
39
|
+
assert.ok(ladderSection().includes(fn), `the ladder names ${fn}`);
|
|
40
|
+
}
|
|
41
|
+
// The two route gates are private to the router, so only their definitions can be checked.
|
|
42
|
+
for (const fn of ['gateOneActiveClaimPerSession', 'gateNewcomerClaim']) {
|
|
43
|
+
assert.match(routes, new RegExp(`function ${fn}\\b`), `routes/claims.js defines ${fn}`);
|
|
44
|
+
}
|
|
45
|
+
assert.match(routes, /router\.post\('\/claims', auth\.requireBuilder, gateOneActiveClaimPerSession, gateNewcomerClaim,/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('every refusal code the ladder names is thrown in db-claims.js', () => {
|
|
49
|
+
const s = ladderSection();
|
|
50
|
+
for (const code of ['BUILDER_INACTIVE', 'REBASE_REQUIRED', 'TASK_NOT_FOUND', 'TASK_NOT_READY', 'OPEN_BLOCKER',
|
|
51
|
+
'rank_too_low_for_task', 'INSUFFICIENT_RANK', 'DEPS_NOT_SHIPPED']) {
|
|
52
|
+
assert.ok(s.includes(code), `ladder names ${code}`);
|
|
53
|
+
assert.ok(dbClaims.includes(`'${code}'`), `db-claims.js carries ${code}`);
|
|
54
|
+
}
|
|
55
|
+
});
|