@bongos/core 1.23.22 → 1.24.1
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 +902 -407
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +5 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +55 -6
- package/docs/page-readings.json +1185 -1011
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +34 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +156 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +24 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +220 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/market-watch/seed.js — load the 2026-10-28 market research into the HUB's
|
|
4
|
+
// Market Watch, once (task 1004936, goal 1000137).
|
|
5
|
+
//
|
|
6
|
+
// WHAT. seed/market-data.json (36 companies scored on the five market dimensions) and
|
|
7
|
+
// seed/partners-data.json (24 philosophy-aligned partners, scored on five values) become
|
|
8
|
+
// tracked items, one 'seed' run, their score snapshots under that run, a few sourced case
|
|
9
|
+
// file entries each, and version 1 of the watch rules. Those two files are the research
|
|
10
|
+
// as it was delivered, committed unchanged; this file is the only reader.
|
|
11
|
+
//
|
|
12
|
+
// WHO GETS IT. The hub only. Every project is its own instance and database (ADR 0393),
|
|
13
|
+
// and this research is about the platform's own market, so a project that turns Market
|
|
14
|
+
// Watch on starts empty and tracks its own names. The hub is the instance that runs
|
|
15
|
+
// platform-identity (the same test src/bongos/routes/feedback.js uses). The project key
|
|
16
|
+
// is the instance's own public origin, read from branding, never a hardcoded name.
|
|
17
|
+
//
|
|
18
|
+
// IDEMPOTENT BY CONSTRAINT, not by a marker. One seed run per project (a partial unique
|
|
19
|
+
// index), items unique on (project, kind, url, name), snapshots on (item, run, dimension),
|
|
20
|
+
// entries on (item, run, takeaway), rules on (project, version): every insert is
|
|
21
|
+
// ON CONFLICT DO NOTHING, all in one transaction. A second load changes nothing, and an
|
|
22
|
+
// item an owner later removed stays removed because its row still exists.
|
|
23
|
+
//
|
|
24
|
+
// NOT A PERSON. Seeded rows carry no author (NULL). The research was a session's work,
|
|
25
|
+
// not a member's act, and an invented author would be a false record.
|
|
26
|
+
//
|
|
27
|
+
// THE BOUNDARY (ADR 0093 §1): core is reached only through ../../src/module-api.js.
|
|
28
|
+
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
|
|
32
|
+
const SEED_DIR = path.join(__dirname, 'seed');
|
|
33
|
+
const RESEARCH_DATE = '2026-10-28';
|
|
34
|
+
const RESEARCH_AT = `${RESEARCH_DATE}T12:00:00Z`;
|
|
35
|
+
|
|
36
|
+
// The two rubrics the research scored on. Keys are the JSON files' own score keys, so
|
|
37
|
+
// a snapshot's `dimension` is the research's word for it. Definitions are the research
|
|
38
|
+
// notes' axes in one line each; weights are 1 (the rules page's "x1"), since the research
|
|
39
|
+
// judged "overall" separately rather than averaging.
|
|
40
|
+
const RUBRIC = Object.freeze({
|
|
41
|
+
competitor: Object.freeze([
|
|
42
|
+
{ key: 'ai_agents_write_code', label: 'AI agents write the code', definition: 'How far AI agents, not people, write and open the code changes.', weight: 1 },
|
|
43
|
+
{ key: 'human_gates', label: 'Humans hold the gates', definition: 'How much people govern the agents: scope, approvals, review and deploy gates.', weight: 1 },
|
|
44
|
+
{ key: 'contributor_economy', label: 'Contributors get paid', definition: 'Whether contributors are paid or credited for the work they ship.', weight: 1 },
|
|
45
|
+
{ key: 'open_source_ethos', label: 'Open source, self-host', definition: 'An open licence and the ability to run it yourself.', weight: 1 },
|
|
46
|
+
{ key: 'multi_project_hub', label: 'Many projects, one hub', definition: 'One place that hosts many projects and their people.', weight: 1 },
|
|
47
|
+
]),
|
|
48
|
+
partner: Object.freeze([
|
|
49
|
+
{ key: 'open_source', label: 'Open source', definition: 'Builds and funds open-source software.', weight: 1 },
|
|
50
|
+
{ key: 'worker_ownership', label: 'Worker ownership', definition: 'Workers or contributors own or steer the organisation.', weight: 1 },
|
|
51
|
+
{ key: 'transparency', label: 'Transparency', definition: 'Decisions, money and records are public.', weight: 1 },
|
|
52
|
+
{ key: 'human_oversight_ai', label: 'Human oversight of AI', definition: 'Insists people review and answer for what AI makes.', weight: 1 },
|
|
53
|
+
{ key: 'cooperative_economics', label: 'Cooperative economics', definition: 'Shares value with the people who create it.', weight: 1 },
|
|
54
|
+
]),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Version 1 of the rules: what the research session searched, read and asked for.
|
|
58
|
+
const RULES_V1 = Object.freeze({
|
|
59
|
+
version: 1,
|
|
60
|
+
summary: 'first rules, from the 2026-10-28 research',
|
|
61
|
+
reason: 'Seeded from the 2026-10-28 market and partner research.',
|
|
62
|
+
searchQueries: [
|
|
63
|
+
'AI agent that writes code and opens pull requests',
|
|
64
|
+
'self-hosted AI software engineer, open source',
|
|
65
|
+
'pay open-source contributors per task',
|
|
66
|
+
'open-source funding and grants for AGPL projects',
|
|
67
|
+
'worker-owned and cooperative software organisations',
|
|
68
|
+
],
|
|
69
|
+
sources: [
|
|
70
|
+
'company sites and pricing pages',
|
|
71
|
+
'changelogs and GitHub releases',
|
|
72
|
+
'funding news, two sources or marked unverified',
|
|
73
|
+
],
|
|
74
|
+
instructions: 'For each tracked item, find what changed this week. Every finding needs a source link and a confidence (low, medium, high); mark single-source claims unverified. Re-score each rubric dimension 0 to 10 and give a one-line reason for any change. Then write five lines for the team: what moved this week, who moved it, and the one thing we should look at. Plain words. No hype.',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function readSeed(name, dir = SEED_DIR) {
|
|
78
|
+
return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hostOf(url) {
|
|
82
|
+
try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isHttp = (u) => typeof u === 'string' && /^https?:\/\//.test(u);
|
|
86
|
+
// A research string that says it is unverified is low confidence; otherwise medium.
|
|
87
|
+
// Nothing in the seed is high: it was one session's desk research, never re-checked.
|
|
88
|
+
const confidenceOf = (text) => (/unverified|unclear|conflict|disagree/i.test(String(text)) ? 1 : 2);
|
|
89
|
+
|
|
90
|
+
// buildSeed({ market, partners }) → { rules, items } — pure: the rows the seed writes,
|
|
91
|
+
// each item carrying its scores and its case-file entries. Tested without a database.
|
|
92
|
+
function buildSeed({ market, partners }) {
|
|
93
|
+
if (!Array.isArray(market) || !Array.isArray(partners)) throw new Error('buildSeed: market and partners must be arrays');
|
|
94
|
+
const items = [];
|
|
95
|
+
for (const c of market) {
|
|
96
|
+
const scores = { ...c.scores, overall: c.overall_likeness };
|
|
97
|
+
const entries = [];
|
|
98
|
+
if (c.what) entries.push({ type: 'insight', takeaway: c.what, confidence: confidenceOf(c.what), dimension: null });
|
|
99
|
+
if (c.pricing) entries.push({ type: 'datapoint', takeaway: `Pricing: ${c.pricing}`, confidence: confidenceOf(c.pricing), dimension: 'contributor_economy' });
|
|
100
|
+
if (c.funding_size) entries.push({ type: 'datapoint', takeaway: `Funding: ${c.funding_size}`, confidence: confidenceOf(c.funding_size), dimension: null });
|
|
101
|
+
if (c.open_source_self_host) entries.push({ type: 'datapoint', takeaway: `Open source / self-host: ${c.open_source_self_host}`, confidence: confidenceOf(c.open_source_self_host), dimension: 'open_source_ethos' });
|
|
102
|
+
items.push({
|
|
103
|
+
kind: 'competitor',
|
|
104
|
+
name: c.name,
|
|
105
|
+
url: c.url,
|
|
106
|
+
category: c.category || '',
|
|
107
|
+
description: c.what || '',
|
|
108
|
+
whyTracked: c.partner_candidate
|
|
109
|
+
? 'A competitor in the 2026-10-28 market research that it also named a possible partner.'
|
|
110
|
+
: 'A competitor in the 2026-10-28 market research.',
|
|
111
|
+
facts: {
|
|
112
|
+
pricing: c.pricing || null,
|
|
113
|
+
funding: c.funding_size || null,
|
|
114
|
+
open_source_self_host: c.open_source_self_host || null,
|
|
115
|
+
partner_candidate: !!c.partner_candidate,
|
|
116
|
+
},
|
|
117
|
+
scores,
|
|
118
|
+
entries: entries.map((e) => ({ ...e, sourceUrl: c.url, sourceLabel: hostOf(c.url) })),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
for (const p of partners) {
|
|
122
|
+
const scores = { ...p.scores, overall: p.overall };
|
|
123
|
+
const src = isHttp(p.quote_url) ? p.quote_url : p.url;
|
|
124
|
+
const entries = [];
|
|
125
|
+
if (p.partnership) entries.push({ type: 'insight', takeaway: p.partnership, confidence: confidenceOf(p.partnership), dimension: null });
|
|
126
|
+
if (p.quote) entries.push({ type: 'datapoint', takeaway: `In their words: "${p.quote}"`, confidence: 3, dimension: null });
|
|
127
|
+
else if (p.quote_note) entries.push({ type: 'datapoint', takeaway: p.quote_note, confidence: 1, dimension: null });
|
|
128
|
+
for (const u of p.unverified || []) entries.push({ type: 'datapoint', takeaway: `Unverified: ${u}`, confidence: 1, dimension: null });
|
|
129
|
+
items.push({
|
|
130
|
+
kind: 'partner',
|
|
131
|
+
name: p.name,
|
|
132
|
+
url: p.url,
|
|
133
|
+
category: p.category || '',
|
|
134
|
+
description: p.partnership || '',
|
|
135
|
+
whyTracked: `A philosophy-aligned partner, ranked ${p.rank} of ${partners.length} in the 2026-10-28 partner research.`,
|
|
136
|
+
facts: { rank: p.rank, quote: p.quote || null, quote_url: p.quote_url || null, unverified: p.unverified || [] },
|
|
137
|
+
scores,
|
|
138
|
+
entries: entries.map((e) => ({ ...e, sourceUrl: src, sourceLabel: hostOf(src) })),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
for (const it of items) {
|
|
142
|
+
if (!isHttp(it.url)) throw new Error(`buildSeed: ${it.name} has no http(s) url`);
|
|
143
|
+
for (const [k, v] of Object.entries(it.scores)) {
|
|
144
|
+
if (typeof v !== 'number' || v < 0 || v > 10) throw new Error(`buildSeed: ${it.name} ${k} score ${v} is not 0..10`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { rules: { ...RULES_V1, rubric: RUBRIC }, items };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// projectKey(origin) — the instance's public origin, lower-cased, no trailing slash: the
|
|
151
|
+
// one spelling every market_watch_* row's `project` uses.
|
|
152
|
+
function projectKey(origin) {
|
|
153
|
+
const s = String(origin || '').trim().replace(/\/+$/, '').toLowerCase();
|
|
154
|
+
return /^https?:\/\/[^/\s]+$/.test(s) ? s : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function defaultIsHub() {
|
|
158
|
+
try { return require('../../src/module-api').isModuleEnabled('platform-identity') === true; } catch { return false; }
|
|
159
|
+
}
|
|
160
|
+
function defaultProject() {
|
|
161
|
+
try { return projectKey(require('../../src/module-api').branding().domains.publicOrigin); } catch { return null; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// writeSeed(client, project, seed) — the inserts, on a client already inside a
|
|
165
|
+
// transaction. Returns counts of rows actually inserted (0 everywhere on a re-run).
|
|
166
|
+
async function writeSeed(client, project, seed) {
|
|
167
|
+
const n = { rules: 0, runs: 0, items: 0, scores: 0, entries: 0 };
|
|
168
|
+
const r = seed.rules;
|
|
169
|
+
const ins = await client.query(
|
|
170
|
+
`INSERT INTO market_watch_rule_versions
|
|
171
|
+
(project, version, status, search_queries, sources, rubric, instructions, summary, reason, approved_at, created_at)
|
|
172
|
+
VALUES ($1, $2, 'active', $3::jsonb, $4::jsonb, $5::jsonb, $6, $7, $8, $9, $9)
|
|
173
|
+
ON CONFLICT (project, version) DO NOTHING
|
|
174
|
+
RETURNING id`,
|
|
175
|
+
[project, r.version, JSON.stringify(r.searchQueries), JSON.stringify(r.sources), JSON.stringify(r.rubric),
|
|
176
|
+
r.instructions, r.summary, r.reason, RESEARCH_AT],
|
|
177
|
+
);
|
|
178
|
+
n.rules += ins.rowCount;
|
|
179
|
+
const { rows: [rule] } = await client.query(
|
|
180
|
+
'SELECT id FROM market_watch_rule_versions WHERE project = $1 AND version = $2', [project, r.version]);
|
|
181
|
+
|
|
182
|
+
const runIns = await client.query(
|
|
183
|
+
`INSERT INTO market_watch_runs (project, status, trigger, rule_version_id, started_at, finished_at, created_at)
|
|
184
|
+
VALUES ($1, 'done', 'seed', $2, $3, $3, $3)
|
|
185
|
+
ON CONFLICT (project) WHERE trigger = 'seed' DO NOTHING
|
|
186
|
+
RETURNING id`,
|
|
187
|
+
[project, rule.id, RESEARCH_AT],
|
|
188
|
+
);
|
|
189
|
+
n.runs += runIns.rowCount;
|
|
190
|
+
const { rows: [run] } = await client.query(
|
|
191
|
+
"SELECT id FROM market_watch_runs WHERE project = $1 AND trigger = 'seed'", [project]);
|
|
192
|
+
|
|
193
|
+
for (const it of seed.items) {
|
|
194
|
+
const itemIns = await client.query(
|
|
195
|
+
`INSERT INTO market_watch_items
|
|
196
|
+
(project, kind, name, url, category, description, why_tracked, facts, added_via, created_at, updated_at)
|
|
197
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, 'seed', $9, $9)
|
|
198
|
+
ON CONFLICT (project, kind, url, name) DO NOTHING
|
|
199
|
+
RETURNING id`,
|
|
200
|
+
[project, it.kind, it.name, it.url, it.category, it.description, it.whyTracked, JSON.stringify(it.facts), RESEARCH_AT],
|
|
201
|
+
);
|
|
202
|
+
n.items += itemIns.rowCount;
|
|
203
|
+
const { rows: [item] } = await client.query(
|
|
204
|
+
'SELECT id FROM market_watch_items WHERE project = $1 AND kind = $2 AND url = $3 AND name = $4',
|
|
205
|
+
[project, it.kind, it.url, it.name]);
|
|
206
|
+
for (const [dimension, value] of Object.entries(it.scores)) {
|
|
207
|
+
const s = await client.query(
|
|
208
|
+
`INSERT INTO market_watch_score_snapshots (item_id, run_id, dimension, value, created_at)
|
|
209
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
210
|
+
ON CONFLICT (item_id, run_id, dimension) DO NOTHING`,
|
|
211
|
+
[item.id, run.id, dimension, value, RESEARCH_AT],
|
|
212
|
+
);
|
|
213
|
+
n.scores += s.rowCount;
|
|
214
|
+
}
|
|
215
|
+
for (const e of it.entries) {
|
|
216
|
+
const s = await client.query(
|
|
217
|
+
`INSERT INTO market_watch_log_entries
|
|
218
|
+
(item_id, run_id, type, found_on, takeaway, dimension, source_url, source_label, confidence, created_at)
|
|
219
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
220
|
+
ON CONFLICT (item_id, run_id, md5(takeaway)) DO NOTHING`,
|
|
221
|
+
[item.id, run.id, e.type, RESEARCH_DATE, e.takeaway, e.dimension, e.sourceUrl, e.sourceLabel, e.confidence, RESEARCH_AT],
|
|
222
|
+
);
|
|
223
|
+
n.entries += s.rowCount;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return n;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// seedHub(deps) → { seeded: true, project, inserted } | { seeded: false, skipped }.
|
|
230
|
+
// deps.pool / isHub / project / seed are test seams; production reads the doorway.
|
|
231
|
+
async function seedHub(deps = {}) {
|
|
232
|
+
const isHub = deps.isHub !== undefined ? deps.isHub : defaultIsHub();
|
|
233
|
+
if (isHub !== true) return { seeded: false, skipped: 'not_hub' };
|
|
234
|
+
const project = deps.project !== undefined ? projectKey(deps.project) : defaultProject();
|
|
235
|
+
if (!project) return { seeded: false, skipped: 'no_project_origin' };
|
|
236
|
+
const pool = deps.pool || require('../../src/module-api').pool;
|
|
237
|
+
const seed = deps.seed || buildSeed({ market: readSeed('market-data.json'), partners: readSeed('partners-data.json') });
|
|
238
|
+
const client = await pool.connect();
|
|
239
|
+
try {
|
|
240
|
+
await client.query('BEGIN');
|
|
241
|
+
const inserted = await writeSeed(client, project, seed);
|
|
242
|
+
await client.query('COMMIT');
|
|
243
|
+
return { seeded: true, project, inserted };
|
|
244
|
+
} catch (e) {
|
|
245
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
246
|
+
throw e;
|
|
247
|
+
} finally {
|
|
248
|
+
client.release();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = { seedHub, writeSeed, buildSeed, readSeed, projectKey, RUBRIC, RULES_V1, RESEARCH_DATE };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/market-watch/store.js — the weekly run's reads and writes (task 1004937, ADR 0394).
|
|
4
|
+
//
|
|
5
|
+
// run.js decides; this file is the only place that spells the run's SQL. Every function
|
|
6
|
+
// takes the project key and every statement filters on it, so a run can only ever read
|
|
7
|
+
// or write its own project's rows.
|
|
8
|
+
//
|
|
9
|
+
// THE CROSS-PROJECT GUARD IS IN THE SQL, not only in the caller. Each item result is
|
|
10
|
+
// written in one transaction that first proves the item AND the run both belong to the
|
|
11
|
+
// project (`guardSql`); if they do not, nothing is written and the call throws
|
|
12
|
+
// `cross_project`. The log entries and score snapshots are then inserted with
|
|
13
|
+
// INSERT ... SELECT ... WHERE i.project = $project, so even a statement that somehow ran
|
|
14
|
+
// past the guard could not land a row on another project's item.
|
|
15
|
+
//
|
|
16
|
+
// WRITTEN ONCE. An item's result moves its run_items row to 'done' or 'error' in the same
|
|
17
|
+
// transaction as its entries and scores, guarded by `status NOT IN ('done','error')`. A
|
|
18
|
+
// second write for the same (run, item) — a resumed run, a second process — finds the
|
|
19
|
+
// row already finished, rolls back, and reports recorded: false, so the caller never
|
|
20
|
+
// mirrors its cost a second time.
|
|
21
|
+
|
|
22
|
+
const db = require('./db');
|
|
23
|
+
|
|
24
|
+
const LEASE_SQL = `now() + ($3::bigint * interval '1 millisecond')`;
|
|
25
|
+
|
|
26
|
+
const SETTINGS_SQL = `SELECT enabled, monthly_cap_usd, cadence FROM market_watch_settings WHERE project = $1`;
|
|
27
|
+
|
|
28
|
+
async function readSettings(pool, project) {
|
|
29
|
+
const { rows: [s] } = await pool.query(SETTINGS_SQL, [project]);
|
|
30
|
+
if (!s) return { ...db.DEFAULT_SETTINGS };
|
|
31
|
+
return { enabled: s.enabled === true, monthly_cap_usd: Number(s.monthly_cap_usd), cadence: s.cadence, configured: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const LAST_RUN_SQL = `
|
|
35
|
+
SELECT id, status, reason, trigger, created_at, claimed_until
|
|
36
|
+
FROM market_watch_runs WHERE project = $1
|
|
37
|
+
ORDER BY created_at DESC, id DESC LIMIT 1`;
|
|
38
|
+
|
|
39
|
+
// The newest run of any kind (the seed run included): what the weekly clock counts from.
|
|
40
|
+
async function lastRun(pool, project) {
|
|
41
|
+
const { rows: [r] } = await pool.query(LAST_RUN_SQL, [project]);
|
|
42
|
+
return r ? { ...r, id: Number(r.id) } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const OPEN_RUN_SQL = `
|
|
46
|
+
SELECT id, status, rule_version_id, cap_usd, claimed_until, (claimed_until IS NOT NULL AND claimed_until > now()) AS leased
|
|
47
|
+
FROM market_watch_runs WHERE project = $1 AND status IN ('queued', 'running')
|
|
48
|
+
ORDER BY id LIMIT 1`;
|
|
49
|
+
|
|
50
|
+
async function openRun(pool, project) {
|
|
51
|
+
const { rows: [r] } = await pool.query(OPEN_RUN_SQL, [project]);
|
|
52
|
+
return r ? { ...r, id: Number(r.id), leased: r.leased === true } : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SKIPPED_SQL = `
|
|
56
|
+
INSERT INTO market_watch_runs (project, status, reason, trigger, rule_version_id, cap_usd, spent_before_usd, finished_at)
|
|
57
|
+
VALUES ($1, 'skipped', $2, $3, $4, $5, $6, now())
|
|
58
|
+
RETURNING id`;
|
|
59
|
+
|
|
60
|
+
// A run that never started is still a row, with its reason (the settings strip shows it).
|
|
61
|
+
async function insertSkipped(pool, project, { reason, trigger = 'poller', ruleVersionId = null, capUsd = null, spentBeforeUsd = null }) {
|
|
62
|
+
const { rows: [r] } = await pool.query(SKIPPED_SQL, [project, reason, trigger, ruleVersionId, capUsd, spentBeforeUsd]);
|
|
63
|
+
return Number(r.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Created already running and leased, in one statement. ON CONFLICT against the
|
|
67
|
+
// one-open-run-per-project index (market_watch_002): a second process that found the same
|
|
68
|
+
// run due gets no row back and stands down.
|
|
69
|
+
const CREATE_SQL = `
|
|
70
|
+
INSERT INTO market_watch_runs (project, status, trigger, rule_version_id, cap_usd, spent_before_usd, claimed_until, started_at)
|
|
71
|
+
VALUES ($1, 'running', $2, $4, $5, $6, ${LEASE_SQL}, now())
|
|
72
|
+
ON CONFLICT (project) WHERE status IN ('queued', 'running') DO NOTHING
|
|
73
|
+
RETURNING id`;
|
|
74
|
+
|
|
75
|
+
async function createRun(pool, project, { trigger = 'poller', ruleVersionId, capUsd, spentBeforeUsd, leaseMs }) {
|
|
76
|
+
const { rows } = await pool.query(CREATE_SQL, [project, trigger, leaseMs, ruleVersionId, capUsd, spentBeforeUsd]);
|
|
77
|
+
return rows[0] ? Number(rows[0].id) : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Take (or retake) an open run whose lease has lapsed: the single-flight claim.
|
|
81
|
+
const CLAIM_SQL = `
|
|
82
|
+
UPDATE market_watch_runs
|
|
83
|
+
SET status = 'running', claimed_until = ${LEASE_SQL}, started_at = COALESCE(started_at, now())
|
|
84
|
+
WHERE project = $1 AND id = $2 AND status IN ('queued', 'running')
|
|
85
|
+
AND (claimed_until IS NULL OR claimed_until < now())
|
|
86
|
+
RETURNING id`;
|
|
87
|
+
|
|
88
|
+
async function claimRun(pool, project, runId, leaseMs) {
|
|
89
|
+
const { rows } = await pool.query(CLAIM_SQL, [project, runId, leaseMs]);
|
|
90
|
+
return rows.length === 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const RENEW_SQL = `
|
|
94
|
+
UPDATE market_watch_runs SET claimed_until = ${LEASE_SQL}
|
|
95
|
+
WHERE project = $1 AND id = $2 AND status = 'running'
|
|
96
|
+
RETURNING id`;
|
|
97
|
+
|
|
98
|
+
async function renewLease(pool, project, runId, leaseMs) {
|
|
99
|
+
const { rows } = await pool.query(RENEW_SQL, [project, runId, leaseMs]);
|
|
100
|
+
return rows.length === 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const FINISH_SQL = `
|
|
104
|
+
UPDATE market_watch_runs SET status = $3, reason = $4, finished_at = now(), claimed_until = NULL
|
|
105
|
+
WHERE project = $1 AND id = $2 AND status IN ('queued', 'running')
|
|
106
|
+
RETURNING id`;
|
|
107
|
+
|
|
108
|
+
async function finishRun(pool, project, runId, { status, reason = null }) {
|
|
109
|
+
const { rows } = await pool.query(FINISH_SQL, [project, runId, status, reason]);
|
|
110
|
+
return rows.length === 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mark an item started. The INSERT ... SELECT only matches when both the run and the item
|
|
114
|
+
// are this project's, and a finished row is never reopened.
|
|
115
|
+
const START_ITEM_SQL = `
|
|
116
|
+
INSERT INTO market_watch_run_items (run_id, item_id, status, started_at)
|
|
117
|
+
SELECT r.id, i.id, 'running', now()
|
|
118
|
+
FROM market_watch_runs r JOIN market_watch_items i ON i.project = r.project
|
|
119
|
+
WHERE r.project = $1 AND r.id = $2::bigint AND i.id = $3::bigint
|
|
120
|
+
ON CONFLICT (run_id, item_id) DO UPDATE SET status = 'running', started_at = now()
|
|
121
|
+
WHERE market_watch_run_items.status NOT IN ('done', 'error')
|
|
122
|
+
RETURNING item_id`;
|
|
123
|
+
|
|
124
|
+
async function startItem(pool, project, runId, itemId) {
|
|
125
|
+
const { rows } = await pool.query(START_ITEM_SQL, [project, runId, itemId]);
|
|
126
|
+
return rows.length === 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const GUARD_SQL = `
|
|
130
|
+
SELECT 1 FROM market_watch_items i
|
|
131
|
+
JOIN market_watch_runs r ON r.project = i.project
|
|
132
|
+
WHERE i.project = $1 AND r.id = $2 AND i.id = $3`;
|
|
133
|
+
|
|
134
|
+
const ENTRY_SQL = `
|
|
135
|
+
INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, dimension, source_url, source_label, confidence)
|
|
136
|
+
SELECT i.id, $3::bigint, $4::text, $5::text, $6::text, $7::text, $8::text, $9::smallint
|
|
137
|
+
FROM market_watch_items i WHERE i.project = $1 AND i.id = $2
|
|
138
|
+
ON CONFLICT (item_id, run_id, md5(takeaway)) DO NOTHING`;
|
|
139
|
+
|
|
140
|
+
const SCORE_SQL = `
|
|
141
|
+
INSERT INTO market_watch_score_snapshots (item_id, run_id, dimension, value, reason)
|
|
142
|
+
SELECT i.id, $3::bigint, $4::text, $5::numeric, $6::text
|
|
143
|
+
FROM market_watch_items i WHERE i.project = $1 AND i.id = $2
|
|
144
|
+
ON CONFLICT (item_id, run_id, dimension) DO NOTHING`;
|
|
145
|
+
|
|
146
|
+
// A candidate is a PROPOSAL to add an item, never an item. Skipped when the project
|
|
147
|
+
// already tracks something by that name; one per name per run (market_watch_002).
|
|
148
|
+
const PROPOSAL_SQL = `
|
|
149
|
+
INSERT INTO market_watch_proposals (project, kind, origin, run_id, payload, reason)
|
|
150
|
+
SELECT $1::text, 'add_item', 'run', $2::bigint, $3::jsonb, $4::text
|
|
151
|
+
WHERE NOT EXISTS (SELECT 1 FROM market_watch_items x WHERE x.project = $1 AND lower(x.name) = lower($5::text))
|
|
152
|
+
ON CONFLICT DO NOTHING`;
|
|
153
|
+
|
|
154
|
+
const FINISH_ITEM_SQL = `
|
|
155
|
+
UPDATE market_watch_run_items SET status = $3, error = $4, cost_usd = $5, finished_at = now()
|
|
156
|
+
WHERE run_id = $1 AND item_id = $2 AND status NOT IN ('done', 'error')
|
|
157
|
+
RETURNING item_id`;
|
|
158
|
+
|
|
159
|
+
// recordItem — one item's whole result, in one transaction. Returns { recorded, counts }.
|
|
160
|
+
// Throws Error('cross_project') if the run or the item is not this project's.
|
|
161
|
+
async function recordItem(pool, project, runId, itemId, result) {
|
|
162
|
+
const r = result || {};
|
|
163
|
+
const client = await pool.connect();
|
|
164
|
+
const counts = { entries: 0, scores: 0, proposals: 0 };
|
|
165
|
+
try {
|
|
166
|
+
await client.query('BEGIN');
|
|
167
|
+
const { rows: guard } = await client.query(GUARD_SQL, [project, runId, itemId]);
|
|
168
|
+
if (guard.length !== 1) {
|
|
169
|
+
await client.query('ROLLBACK');
|
|
170
|
+
const e = new Error('cross_project');
|
|
171
|
+
e.code = 'cross_project';
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
if (r.ok) {
|
|
175
|
+
for (const e of r.entries || []) {
|
|
176
|
+
const x = await client.query(ENTRY_SQL, [project, itemId, runId, e.type, e.takeaway, e.dimension, e.sourceUrl, e.sourceLabel, e.confidence]);
|
|
177
|
+
counts.entries += x.rowCount || 0;
|
|
178
|
+
}
|
|
179
|
+
for (const s of r.scores || []) {
|
|
180
|
+
const x = await client.query(SCORE_SQL, [project, itemId, runId, s.dimension, s.value, s.reason]);
|
|
181
|
+
counts.scores += x.rowCount || 0;
|
|
182
|
+
}
|
|
183
|
+
for (const c of r.candidates || []) {
|
|
184
|
+
const payload = { name: c.name, url: c.url, kind: c.kind, found_via_item_id: itemId };
|
|
185
|
+
const x = await client.query(PROPOSAL_SQL, [project, runId, JSON.stringify(payload), c.why, c.name]);
|
|
186
|
+
counts.proposals += x.rowCount || 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const { rows: fin } = await client.query(FINISH_ITEM_SQL, [
|
|
190
|
+
runId, itemId, r.ok ? 'done' : 'error', r.ok ? null : String(r.error || 'error').slice(0, 200), r.costUsd || 0,
|
|
191
|
+
]);
|
|
192
|
+
if (fin.length !== 1) {
|
|
193
|
+
// Already finished by an earlier write: keep that one, write nothing new.
|
|
194
|
+
await client.query('ROLLBACK');
|
|
195
|
+
return { recorded: false, counts: { entries: 0, scores: 0, proposals: 0 } };
|
|
196
|
+
}
|
|
197
|
+
await client.query('COMMIT');
|
|
198
|
+
return { recorded: true, counts };
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e && e.code !== 'cross_project') await client.query('ROLLBACK').catch(() => {});
|
|
201
|
+
throw e;
|
|
202
|
+
} finally {
|
|
203
|
+
client.release();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The store bound to one pool: what run.js is handed. Tests hand run.js a fake with the
|
|
208
|
+
// same method names.
|
|
209
|
+
function createStore(pool) {
|
|
210
|
+
return {
|
|
211
|
+
readSettings: (project) => readSettings(pool, project),
|
|
212
|
+
lastRun: (project) => lastRun(pool, project),
|
|
213
|
+
openRun: (project) => openRun(pool, project),
|
|
214
|
+
insertSkipped: (project, o) => insertSkipped(pool, project, o),
|
|
215
|
+
createRun: (project, o) => createRun(pool, project, o),
|
|
216
|
+
claimRun: (project, runId, leaseMs) => claimRun(pool, project, runId, leaseMs),
|
|
217
|
+
renewLease: (project, runId, leaseMs) => renewLease(pool, project, runId, leaseMs),
|
|
218
|
+
finishRun: (project, runId, o) => finishRun(pool, project, runId, o),
|
|
219
|
+
startItem: (project, runId, itemId) => startItem(pool, project, runId, itemId),
|
|
220
|
+
recordItem: (project, runId, itemId, result) => recordItem(pool, project, runId, itemId, result),
|
|
221
|
+
remainingItems: (project, runId) => db.remainingItems(pool, { project, runId }),
|
|
222
|
+
monthSpend: (project, now) => db.monthSpend(pool, { project, now }),
|
|
223
|
+
activeRules: async (project) => (await db.rules(pool, { project })).active,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
createStore, openRun, recordItem,
|
|
229
|
+
SQL: { GUARD_SQL, ENTRY_SQL, SCORE_SQL, PROPOSAL_SQL, START_ITEM_SQL, FINISH_ITEM_SQL, CREATE_SQL },
|
|
230
|
+
};
|
|
@@ -78,6 +78,15 @@ const db = api;
|
|
|
78
78
|
const SYNC_RATE_WINDOW_MS = 60 * 1000;
|
|
79
79
|
const SYNC_RATE_LIMIT = 6; // syncs per builder per minute
|
|
80
80
|
const syncBuckets = new Map(); // builderId -> timestamp[]
|
|
81
|
+
// Hard size cap on both bucket maps here (task 1004964): their sweeps bound them by
|
|
82
|
+
// time only, every five minutes. At the cap the oldest-inserted key goes first.
|
|
83
|
+
const MAX_TRACKED_KEYS = 50000;
|
|
84
|
+
function newBucket(map, key) {
|
|
85
|
+
while (map.size >= MAX_TRACKED_KEYS) map.delete(map.keys().next().value);
|
|
86
|
+
const bucket = [];
|
|
87
|
+
map.set(key, bucket);
|
|
88
|
+
return bucket;
|
|
89
|
+
}
|
|
81
90
|
|
|
82
91
|
const syncSweepTimer = setInterval(() => {
|
|
83
92
|
const cutoff = Date.now() - SYNC_RATE_WINDOW_MS;
|
|
@@ -94,7 +103,7 @@ function syncRateLimit(req, res, next) {
|
|
|
94
103
|
const now = Date.now();
|
|
95
104
|
const key = String(req.builder.id);
|
|
96
105
|
let bucket = syncBuckets.get(key);
|
|
97
|
-
if (!bucket)
|
|
106
|
+
if (!bucket) bucket = newBucket(syncBuckets, key);
|
|
98
107
|
const cutoff = now - SYNC_RATE_WINDOW_MS;
|
|
99
108
|
while (bucket.length && bucket[0] < cutoff) bucket.shift();
|
|
100
109
|
if (bucket.length >= SYNC_RATE_LIMIT) {
|
|
@@ -137,7 +146,7 @@ function readRateLimit(req, res, next) {
|
|
|
137
146
|
const now = Date.now();
|
|
138
147
|
const key = String(req.builder.id);
|
|
139
148
|
let bucket = readBuckets.get(key);
|
|
140
|
-
if (!bucket)
|
|
149
|
+
if (!bucket) bucket = newBucket(readBuckets, key);
|
|
141
150
|
const cutoff = now - READ_RATE_WINDOW_MS;
|
|
142
151
|
while (bucket.length && bucket[0] < cutoff) bucket.shift();
|
|
143
152
|
if (bucket.length >= READ_RATE_LIMIT) {
|
|
@@ -914,5 +923,8 @@ module.exports._internals = {
|
|
|
914
923
|
syncRateLimit,
|
|
915
924
|
READ_RATE_LIMIT,
|
|
916
925
|
SYNC_RATE_LIMIT,
|
|
926
|
+
MAX_TRACKED_KEYS,
|
|
927
|
+
syncBuckets,
|
|
928
|
+
readBuckets,
|
|
917
929
|
bfgWriteEnabled,
|
|
918
930
|
};
|
|
@@ -27,6 +27,9 @@ const search = require('../search');
|
|
|
27
27
|
const READ_RATE_WINDOW_MS = 60 * 1000;
|
|
28
28
|
const READ_RATE_LIMIT = 60; // searches per builder per minute
|
|
29
29
|
const buckets = new Map(); // builderId -> timestamp[]
|
|
30
|
+
// Hard size cap (task 1004964): the sweep below bounds the map by time only, every
|
|
31
|
+
// five minutes. At the cap the oldest-inserted key goes first; the newest survive.
|
|
32
|
+
const MAX_TRACKED_KEYS = 50000;
|
|
30
33
|
|
|
31
34
|
const sweepTimer = setInterval(() => {
|
|
32
35
|
const cutoff = Date.now() - READ_RATE_WINDOW_MS;
|
|
@@ -43,7 +46,10 @@ function searchRateLimit(req, res, next) {
|
|
|
43
46
|
const now = Date.now();
|
|
44
47
|
const key = String(req.builder.id);
|
|
45
48
|
let bucket = buckets.get(key);
|
|
46
|
-
if (!bucket) {
|
|
49
|
+
if (!bucket) {
|
|
50
|
+
while (buckets.size >= MAX_TRACKED_KEYS) buckets.delete(buckets.keys().next().value);
|
|
51
|
+
bucket = []; buckets.set(key, bucket);
|
|
52
|
+
}
|
|
47
53
|
const cutoff = now - READ_RATE_WINDOW_MS;
|
|
48
54
|
while (bucket.length && bucket[0] < cutoff) bucket.shift();
|
|
49
55
|
if (bucket.length >= READ_RATE_LIMIT) {
|
|
@@ -212,4 +218,4 @@ module.exports = function buildSearchRouter() {
|
|
|
212
218
|
};
|
|
213
219
|
|
|
214
220
|
// Exported for tests — the pure rate-limit bucket logic + limits.
|
|
215
|
-
module.exports._internals = { searchRateLimit, READ_RATE_LIMIT, READ_RATE_WINDOW_MS };
|
|
221
|
+
module.exports._internals = { searchRateLimit, READ_RATE_LIMIT, READ_RATE_WINDOW_MS, MAX_TRACKED_KEYS, buckets };
|