@bongos/core 1.23.21 → 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 +983 -428
- 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 +6 -3
- 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 +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -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/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- 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 +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- 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 +4 -0
- package/docs/module-api-changelog.md +7 -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/CLAUDE.md +15 -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/module.json +26 -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 +252 -0
- 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 +108 -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 +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- 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/city.html +15 -15
- 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-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- 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/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- 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/onboarding/howto-moments.js +8 -1
- 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 +8 -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 +286 -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 +4 -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 +129 -12
- 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 +16 -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/src/platform-server.js +27 -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/batched_writes_parity.mjs +340 -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/conductor_main_e2e.mjs +4 -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/per-row-loops-legacy.cjs +245 -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_archive_disposition.mjs +5 -5
- 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 +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -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/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- 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/module_skills_hook.mjs +69 -0
- 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/public_landing_projects.mjs +37 -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/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- 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 +58 -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_close_route.mjs +1 -1
- 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,342 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/payout-approvals.js — every payout needs a logged, personal approval
|
|
4
|
+
// (task 1004913, goal 1000092 N9; ADR 0385 decisions 4 and 8, ADR 0387 decision 3).
|
|
5
|
+
//
|
|
6
|
+
// WHY. The owner's legal framework (Art. I, cited by Article only) holds that a
|
|
7
|
+
// payout, an equity grant or a contract is never executed by the platform or an AI
|
|
8
|
+
// on its own: each needs a distinct step by the project's authorised person, and an
|
|
9
|
+
// append-only log of who approved what and when. If we cannot prove the person
|
|
10
|
+
// pressed the button, we were the actor. This file is that step and that log; the
|
|
11
|
+
// money-moving tasks (M04 pays a builder, N6 module sales, N7 cash-out) call
|
|
12
|
+
// checkRun / checkApproval before they instruct the processor, and never move a
|
|
13
|
+
// cent on anything else.
|
|
14
|
+
//
|
|
15
|
+
// THE SHAPE (migrations/core_283_payout_approvals.sql). One append-only table of
|
|
16
|
+
// events. A standing payout rule is its `rule_signed` row; it is narrowed or
|
|
17
|
+
// revoked by later rows, never edited. Runs under a rule are `run_noticed` (the
|
|
18
|
+
// one kind a runner may file: it authorises nothing) and become payable only after
|
|
19
|
+
// the rule's pause window, unless a person files `run_held`. One-off payouts,
|
|
20
|
+
// equity grants and contracts are one row and one signature each — there is no
|
|
21
|
+
// batch form, on purpose.
|
|
22
|
+
//
|
|
23
|
+
// WHO. The routes gate on the `payout.approve` permission (rank read fresh per
|
|
24
|
+
// request, ADR 0016) and then demand a PERSONAL session: the person's own web
|
|
25
|
+
// session, never a CLI token (that is what an AI session runs on), never a machine
|
|
26
|
+
// principal. The database repeats both rules (a CHECK on session_source and a
|
|
27
|
+
// trigger on builders.system_role), so a route that forgot cannot write a
|
|
28
|
+
// signature an AI made. An AI may never create or widen a rule: creating needs a
|
|
29
|
+
// personal session, and widening does not exist — a narrowing that would widen is
|
|
30
|
+
// refused here AND by the trigger, so the only way to a bigger rule is a person
|
|
31
|
+
// signing a new one.
|
|
32
|
+
//
|
|
33
|
+
// Nothing here moves money (ADR 0385 D1): this is the authority a processor
|
|
34
|
+
// transfer must cite. Pure validators first, then the db half (pool injectable).
|
|
35
|
+
|
|
36
|
+
const MAX_CENTS = 100_000_000_00; // $100M: a sanity ceiling, not a policy
|
|
37
|
+
const MAX_PAUSE_HOURS = 720;
|
|
38
|
+
const FORMULA_KINDS = Object.freeze(['task_pay_terms', 'fixed_per_task', 'credit_share']);
|
|
39
|
+
const SCHEDULES = Object.freeze(['on_ship', 'weekly', 'monthly']);
|
|
40
|
+
const CAP_KEYS = Object.freeze(['per_payout_cents', 'per_run_cents', 'per_month_cents']);
|
|
41
|
+
const SIGNED_KINDS = Object.freeze(['rule_signed', 'rule_narrowed', 'rule_revoked', 'run_held', 'payout_approved', 'grant_signed', 'contract_signed']);
|
|
42
|
+
const RUN_KEY_RE = /^[A-Za-z0-9._:-]{1,80}$/;
|
|
43
|
+
const CURRENCY_RE = /^[A-Z]{3}$/;
|
|
44
|
+
const NOTE_MAX = 500;
|
|
45
|
+
|
|
46
|
+
const isCents = (v) => Number.isSafeInteger(v) && v > 0 && v <= MAX_CENTS;
|
|
47
|
+
const isId = (v) => (typeof v === 'number' && Number.isSafeInteger(v) && v > 0) || (typeof v === 'string' && /^[1-9][0-9]{0,18}$/.test(v));
|
|
48
|
+
|
|
49
|
+
function cleanNote(raw) {
|
|
50
|
+
if (raw === undefined || raw === null || raw === '') return { value: null };
|
|
51
|
+
if (typeof raw !== 'string') return { error: 'note must be text' };
|
|
52
|
+
const v = raw.replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (v.length > NOTE_MAX) return { error: `note must be ${NOTE_MAX} characters or fewer` };
|
|
54
|
+
return { value: v || null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A standing payout rule's terms, strictly: every field required that a run is
|
|
58
|
+
// checked against, nothing unknown accepted (a person signs exactly what is stored).
|
|
59
|
+
function validateRuleTerms(raw) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { errors: ['terms must be an object'] };
|
|
62
|
+
const known = new Set(['caps', 'currency', 'formula', 'schedule', 'pause_hours']);
|
|
63
|
+
for (const k of Object.keys(raw)) if (!known.has(k)) errors.push(`unknown term: ${k}`);
|
|
64
|
+
|
|
65
|
+
const caps = raw.caps;
|
|
66
|
+
const outCaps = {};
|
|
67
|
+
if (!caps || typeof caps !== 'object' || Array.isArray(caps)) {
|
|
68
|
+
errors.push('caps must be an object');
|
|
69
|
+
} else {
|
|
70
|
+
for (const k of Object.keys(caps)) if (!CAP_KEYS.includes(k)) errors.push(`unknown cap: ${k}`);
|
|
71
|
+
for (const k of ['per_payout_cents', 'per_run_cents']) {
|
|
72
|
+
if (!isCents(caps[k])) errors.push(`caps.${k} must be a whole number of cents above zero`);
|
|
73
|
+
else outCaps[k] = caps[k];
|
|
74
|
+
}
|
|
75
|
+
if (caps.per_month_cents !== undefined && caps.per_month_cents !== null) {
|
|
76
|
+
if (!isCents(caps.per_month_cents)) errors.push('caps.per_month_cents must be a whole number of cents above zero, or left out');
|
|
77
|
+
else outCaps.per_month_cents = caps.per_month_cents;
|
|
78
|
+
}
|
|
79
|
+
if (outCaps.per_payout_cents && outCaps.per_run_cents && outCaps.per_payout_cents > outCaps.per_run_cents) {
|
|
80
|
+
errors.push('caps.per_payout_cents may not be above caps.per_run_cents');
|
|
81
|
+
}
|
|
82
|
+
if (outCaps.per_run_cents && outCaps.per_month_cents && outCaps.per_run_cents > outCaps.per_month_cents) {
|
|
83
|
+
errors.push('caps.per_run_cents may not be above caps.per_month_cents');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof raw.currency !== 'string' || !CURRENCY_RE.test(raw.currency)) errors.push('currency must be a three-letter code such as USD');
|
|
88
|
+
|
|
89
|
+
const f = raw.formula;
|
|
90
|
+
let formula = null;
|
|
91
|
+
if (!f || typeof f !== 'object' || Array.isArray(f) || !FORMULA_KINDS.includes(f.kind)) {
|
|
92
|
+
errors.push(`formula.kind must be one of ${FORMULA_KINDS.join(', ')}`);
|
|
93
|
+
} else if (f.kind === 'fixed_per_task') {
|
|
94
|
+
if (!isCents(f.amount_cents)) errors.push('formula.amount_cents must be a whole number of cents above zero');
|
|
95
|
+
else formula = { kind: f.kind, amount_cents: f.amount_cents };
|
|
96
|
+
for (const k of Object.keys(f)) if (k !== 'kind' && k !== 'amount_cents') errors.push(`unknown formula field: ${k}`);
|
|
97
|
+
} else if (f.kind === 'credit_share') {
|
|
98
|
+
// ADR 0387 D5: a pool run names the credit row kinds it counts.
|
|
99
|
+
const kinds = f.credit_row_kinds;
|
|
100
|
+
if (!Array.isArray(kinds) || kinds.length === 0 || kinds.some((k) => typeof k !== 'string' || !/^[a-z_.]{1,40}$/.test(k))) {
|
|
101
|
+
errors.push('formula.credit_row_kinds must name the credit row kinds the pool counts');
|
|
102
|
+
} else {
|
|
103
|
+
formula = { kind: f.kind, credit_row_kinds: [...new Set(kinds)].sort() };
|
|
104
|
+
}
|
|
105
|
+
for (const k of Object.keys(f)) if (k !== 'kind' && k !== 'credit_row_kinds') errors.push(`unknown formula field: ${k}`);
|
|
106
|
+
} else {
|
|
107
|
+
formula = { kind: f.kind };
|
|
108
|
+
for (const k of Object.keys(f)) if (k !== 'kind') errors.push(`unknown formula field: ${k}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!SCHEDULES.includes(raw.schedule)) errors.push(`schedule must be one of ${SCHEDULES.join(', ')}`);
|
|
112
|
+
if (!Number.isSafeInteger(raw.pause_hours) || raw.pause_hours < 1 || raw.pause_hours > MAX_PAUSE_HOURS) {
|
|
113
|
+
errors.push(`pause_hours must be a whole number from 1 to ${MAX_PAUSE_HOURS}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (errors.length) return { errors };
|
|
117
|
+
return { terms: { caps: outCaps, currency: raw.currency, formula, schedule: raw.schedule, pause_hours: raw.pause_hours } };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The narrowing rule, the same one the trigger enforces: caps only go down (and a
|
|
121
|
+
// set cap is never removed), the pause only gets longer, and the formula,
|
|
122
|
+
// schedule and currency do not move. Returns the reasons a change would widen.
|
|
123
|
+
function wideningReasons(current, next) {
|
|
124
|
+
const out = [];
|
|
125
|
+
if (JSON.stringify(current.formula) !== JSON.stringify(next.formula)) out.push('the formula would change');
|
|
126
|
+
if (current.schedule !== next.schedule) out.push('the schedule would change');
|
|
127
|
+
if (current.currency !== next.currency) out.push('the currency would change');
|
|
128
|
+
if (next.pause_hours < current.pause_hours) out.push('the pause window would get shorter');
|
|
129
|
+
for (const k of CAP_KEYS) {
|
|
130
|
+
const was = current.caps[k];
|
|
131
|
+
const now = next.caps[k];
|
|
132
|
+
if (was !== undefined && was !== null && (now === undefined || now === null)) out.push(`the ${k} cap would be removed`);
|
|
133
|
+
else if (was !== undefined && was !== null && now > was) out.push(`the ${k} cap would go up`);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// One personal act: the person's own web session, a human builder. Everything a
|
|
139
|
+
// route needs to refuse an AI, in one place (the DB repeats it).
|
|
140
|
+
function personalSession(req) {
|
|
141
|
+
const s = req && req.gdsSession;
|
|
142
|
+
const b = req && req.builder;
|
|
143
|
+
if (!b || !s) return { ok: false, reason: 'no_session' };
|
|
144
|
+
if (b.system_role) return { ok: false, reason: 'machine_principal' };
|
|
145
|
+
if (s.source !== 'web') return { ok: false, reason: 'not_a_web_session' };
|
|
146
|
+
if (s.session_id === undefined || s.session_id === null) return { ok: false, reason: 'no_session' };
|
|
147
|
+
return { ok: true, actor: { id: b.id, rank: b.rank || null, session_id: s.session_id, session_source: 'web' } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fold a rule's events into its current state. events: its rows, oldest first,
|
|
151
|
+
// the signing row included.
|
|
152
|
+
function foldRule(events) {
|
|
153
|
+
const signed = events.find((e) => e.kind === 'rule_signed');
|
|
154
|
+
if (!signed) return null;
|
|
155
|
+
let terms = signed.terms;
|
|
156
|
+
let revoked = null;
|
|
157
|
+
for (const e of events) {
|
|
158
|
+
if (e.kind === 'rule_narrowed') terms = e.terms;
|
|
159
|
+
if (e.kind === 'rule_revoked') revoked = { by: e.actor_id, at: e.created_at };
|
|
160
|
+
}
|
|
161
|
+
return { id: signed.id, signed_by: signed.actor_id, signed_at: signed.created_at, terms, revoked, active: !revoked };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const MONTH = (d) => new Date(d).toISOString().slice(0, 7);
|
|
165
|
+
|
|
166
|
+
// May this run pay now? rule: foldRule's state; ruleEvents: the rule's rows;
|
|
167
|
+
// payouts: [{ recipient_id, amount_cents }]. Pure; checkRun reads the rows.
|
|
168
|
+
function judgeRun({ rule, ruleEvents, runKey, payouts, now = new Date() }) {
|
|
169
|
+
if (!rule) return { ok: false, reason: 'no_such_rule' };
|
|
170
|
+
if (!rule.active) return { ok: false, reason: 'rule_revoked' };
|
|
171
|
+
const notice = ruleEvents.find((e) => e.kind === 'run_noticed' && e.run_key === runKey);
|
|
172
|
+
if (!notice) return { ok: false, reason: 'run_not_noticed' };
|
|
173
|
+
if (ruleEvents.some((e) => e.kind === 'run_held' && e.run_key === runKey)) return { ok: false, reason: 'run_held' };
|
|
174
|
+
const opensAt = new Date(new Date(notice.created_at).getTime() + rule.terms.pause_hours * 3600_000);
|
|
175
|
+
if (now < opensAt) return { ok: false, reason: 'in_pause_window', opens_at: opensAt.toISOString() };
|
|
176
|
+
const noticed = notice.terms && Array.isArray(notice.terms.payouts) ? notice.terms.payouts : [];
|
|
177
|
+
const key = (list) => JSON.stringify(list.map((p) => [String(p.recipient_id), Number(p.amount_cents)]).sort());
|
|
178
|
+
if (!Array.isArray(payouts) || key(payouts) !== key(noticed)) return { ok: false, reason: 'run_differs_from_notice' };
|
|
179
|
+
const caps = rule.terms.caps;
|
|
180
|
+
if (noticed.some((p) => Number(p.amount_cents) > caps.per_payout_cents)) return { ok: false, reason: 'over_per_payout_cap' };
|
|
181
|
+
if (Number(notice.amount_cents) > caps.per_run_cents) return { ok: false, reason: 'over_per_run_cap' };
|
|
182
|
+
if (caps.per_month_cents) {
|
|
183
|
+
const month = MONTH(notice.created_at);
|
|
184
|
+
const held = new Set(ruleEvents.filter((e) => e.kind === 'run_held').map((e) => e.run_key));
|
|
185
|
+
const total = ruleEvents
|
|
186
|
+
.filter((e) => e.kind === 'run_noticed' && !held.has(e.run_key) && MONTH(e.created_at) === month && e.id <= notice.id)
|
|
187
|
+
.reduce((s, e) => s + Number(e.amount_cents), 0);
|
|
188
|
+
if (total > caps.per_month_cents) return { ok: false, reason: 'over_per_month_cap' };
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, rule_id: rule.id, notice_id: notice.id, currency: notice.currency, payouts: noticed };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── db half ────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function defaultPool() { return require('./pool').pool; }
|
|
196
|
+
|
|
197
|
+
const COLS = 'id, kind, rule_id, run_key, actor_id, actor_rank, session_id, session_source, recipient_id, amount_cents, currency, terms, note, created_at';
|
|
198
|
+
|
|
199
|
+
function shapeRow(r) {
|
|
200
|
+
if (!r) return null;
|
|
201
|
+
return {
|
|
202
|
+
...r,
|
|
203
|
+
id: String(r.id),
|
|
204
|
+
rule_id: r.rule_id == null ? null : String(r.rule_id),
|
|
205
|
+
actor_id: String(r.actor_id),
|
|
206
|
+
recipient_id: r.recipient_id == null ? null : String(r.recipient_id),
|
|
207
|
+
session_id: r.session_id == null ? null : String(r.session_id),
|
|
208
|
+
amount_cents: r.amount_cents == null ? null : Number(r.amount_cents),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function insertEvent(e, { pool = defaultPool() } = {}) {
|
|
213
|
+
const { rows } = await pool.query(
|
|
214
|
+
`INSERT INTO payout_approvals (kind, rule_id, run_key, actor_id, actor_rank, session_id, session_source,
|
|
215
|
+
recipient_id, amount_cents, currency, terms, note)
|
|
216
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12)
|
|
217
|
+
RETURNING ${COLS}`,
|
|
218
|
+
[e.kind, e.rule_id ?? null, e.run_key ?? null, e.actor.id, e.actor.rank ?? null, e.actor.session_id ?? null,
|
|
219
|
+
e.actor.session_source ?? null, e.recipient_id ?? null, e.amount_cents ?? null, e.currency ?? null,
|
|
220
|
+
JSON.stringify(e.terms || {}), e.note ?? null],
|
|
221
|
+
);
|
|
222
|
+
return shapeRow(rows[0]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function ruleEvents(ruleId, { pool = defaultPool() } = {}) {
|
|
226
|
+
const { rows } = await pool.query(
|
|
227
|
+
`SELECT ${COLS} FROM payout_approvals WHERE id = $1 OR rule_id = $1 ORDER BY id`,
|
|
228
|
+
[ruleId],
|
|
229
|
+
);
|
|
230
|
+
return rows.map(shapeRow);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function readRule(ruleId, opts = {}) {
|
|
234
|
+
const events = await ruleEvents(ruleId, opts);
|
|
235
|
+
return { rule: foldRule(events), events };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function listRules({ pool = defaultPool() } = {}) {
|
|
239
|
+
const { rows } = await pool.query(
|
|
240
|
+
`SELECT ${COLS} FROM payout_approvals
|
|
241
|
+
WHERE kind IN ('rule_signed', 'rule_narrowed', 'rule_revoked')
|
|
242
|
+
ORDER BY id`,
|
|
243
|
+
);
|
|
244
|
+
const byRule = new Map();
|
|
245
|
+
for (const r of rows.map(shapeRow)) {
|
|
246
|
+
const id = r.kind === 'rule_signed' ? r.id : r.rule_id;
|
|
247
|
+
if (!byRule.has(id)) byRule.set(id, []);
|
|
248
|
+
byRule.get(id).push(r);
|
|
249
|
+
}
|
|
250
|
+
return [...byRule.values()].map(foldRule).filter(Boolean);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function listLog({ ruleId = null, limit = 100, pool = defaultPool() } = {}) {
|
|
254
|
+
const lim = Math.min(Math.max(Number(limit) || 100, 1), 500);
|
|
255
|
+
const { rows } = ruleId
|
|
256
|
+
? await pool.query(`SELECT ${COLS} FROM payout_approvals WHERE id = $1 OR rule_id = $1 ORDER BY id DESC LIMIT $2`, [ruleId, lim])
|
|
257
|
+
: await pool.query(`SELECT ${COLS} FROM payout_approvals ORDER BY id DESC LIMIT $1`, [lim]);
|
|
258
|
+
return rows.map(shapeRow);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// For M04 and friends: may this run under this rule pay now?
|
|
262
|
+
// The N10 gates (age tier, sanctions, open holds) are checked again at pay time,
|
|
263
|
+
// because a recipient's standing can change inside a pause window. Injectable so
|
|
264
|
+
// a test can stand in for the database; production reads ./payout-gates.
|
|
265
|
+
function gatesOf(opts) { return opts.gates || require('./payout-gates'); }
|
|
266
|
+
|
|
267
|
+
// Returns { ok, payouts, held }: `payouts` are the lines that may be paid now
|
|
268
|
+
// (each with pay_to — the guardian's account for a 16-17 recipient), `held` the
|
|
269
|
+
// lines that may not, each already notified with a hold the person can appeal.
|
|
270
|
+
// A run is never stopped whole for one recipient; their line waits, the rest pay.
|
|
271
|
+
async function checkRun({ ruleId, runKey, payouts, now = new Date() }, opts = {}) {
|
|
272
|
+
const { rule, events } = await readRule(ruleId, opts);
|
|
273
|
+
const v = judgeRun({ rule, ruleEvents: events, runKey, payouts, now });
|
|
274
|
+
if (!v.ok) return v;
|
|
275
|
+
const gates = gatesOf(opts);
|
|
276
|
+
// One batched read for the whole run, not a round of reads per recipient.
|
|
277
|
+
const owed = new Map();
|
|
278
|
+
for (const p of v.payouts) owed.set(String(p.recipient_id), (owed.get(String(p.recipient_id)) || 0) + Number(p.amount_cents));
|
|
279
|
+
const verdicts = await gates.gateOwedPayMany(owed, { now, pool: opts.pool });
|
|
280
|
+
const minors = [...verdicts].filter(([, g]) => g.ok && g.needs_guardian_cosign).map(([rid]) => rid);
|
|
281
|
+
const cosigned = minors.length ? await gates.cosignedInRun({ ruleId: v.rule_id, runKey, builderIds: minors }, { pool: opts.pool }) : new Set();
|
|
282
|
+
const pay = [];
|
|
283
|
+
const held = [];
|
|
284
|
+
for (const p of v.payouts) {
|
|
285
|
+
const g = verdicts.get(String(p.recipient_id));
|
|
286
|
+
if (!g.ok) { held.push({ ...p, reason: g.reason, freeze_id: g.freeze_id }); continue; }
|
|
287
|
+
if (g.needs_guardian_cosign && !cosigned.has(String(p.recipient_id))) { held.push({ ...p, reason: 'guardian_cosign_missing', freeze_id: null }); continue; }
|
|
288
|
+
pay.push({ ...p, pay_to: g.pay_to });
|
|
289
|
+
}
|
|
290
|
+
return { ...v, payouts: pay, held };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// For a one-off: does approval `id` cover exactly this payout, grant or contract?
|
|
294
|
+
// It answers whether the authority EXISTS; it does not spend it. The paying task
|
|
295
|
+
// must make the transfer idempotent on the approval id (one approval, one
|
|
296
|
+
// transfer: the processor idempotency key and its own money record, ADR 0387 D2),
|
|
297
|
+
// or a second call would pay twice. checkRun is the same, keyed on rule + run_key.
|
|
298
|
+
async function checkApproval({ id, kind = 'payout_approved', recipientId, amountCents = null, currency = null, now = new Date() }, opts = {}) {
|
|
299
|
+
const pool = opts.pool || defaultPool();
|
|
300
|
+
if (!isId(id)) return { ok: false, reason: 'no_such_approval' };
|
|
301
|
+
const { rows } = await pool.query(`SELECT ${COLS} FROM payout_approvals WHERE id = $1`, [id]);
|
|
302
|
+
const row = shapeRow(rows[0]);
|
|
303
|
+
if (!row || row.kind !== kind) return { ok: false, reason: 'no_such_approval' };
|
|
304
|
+
if (String(row.recipient_id) !== String(recipientId)) return { ok: false, reason: 'different_recipient' };
|
|
305
|
+
if (kind === 'payout_approved' && (row.amount_cents !== Number(amountCents) || row.currency !== currency)) {
|
|
306
|
+
return { ok: false, reason: 'different_amount' };
|
|
307
|
+
}
|
|
308
|
+
// N10: the recipient must still pass the gates when it pays. Money owed and
|
|
309
|
+
// refused files a hold that tells them; a grant or contract refuses without one.
|
|
310
|
+
const gates = gatesOf(opts);
|
|
311
|
+
const g = kind === 'payout_approved'
|
|
312
|
+
? await gates.gateOwedPay(row.recipient_id, { owedCents: row.amount_cents, now, pool })
|
|
313
|
+
: await gates.gateRecipient(row.recipient_id, { now, pool });
|
|
314
|
+
if (!g.ok) return { ok: false, reason: 'recipient_gated', gate: { reason: g.reason, freeze_id: g.freeze_id || null } };
|
|
315
|
+
if (g.needs_guardian_cosign && !(await gates.hasCosign({ builderId: row.recipient_id, approvalId: row.id }, { pool }))) {
|
|
316
|
+
return { ok: false, reason: 'guardian_cosign_missing' };
|
|
317
|
+
}
|
|
318
|
+
return { ok: true, approval: row, pay_to: g.pay_to };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
FORMULA_KINDS,
|
|
323
|
+
SCHEDULES,
|
|
324
|
+
CAP_KEYS,
|
|
325
|
+
SIGNED_KINDS,
|
|
326
|
+
RUN_KEY_RE,
|
|
327
|
+
MAX_PAUSE_HOURS,
|
|
328
|
+
isCents,
|
|
329
|
+
isId,
|
|
330
|
+
cleanNote,
|
|
331
|
+
validateRuleTerms,
|
|
332
|
+
wideningReasons,
|
|
333
|
+
personalSession,
|
|
334
|
+
foldRule,
|
|
335
|
+
judgeRun,
|
|
336
|
+
insertEvent,
|
|
337
|
+
readRule,
|
|
338
|
+
listRules,
|
|
339
|
+
listLog,
|
|
340
|
+
checkRun,
|
|
341
|
+
checkApproval,
|
|
342
|
+
};
|