@bongos/core 1.23.22 → 1.24.2
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 +933 -413
- 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 +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -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 +12 -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 +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -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 +57 -7
- package/docs/page-readings.json +1210 -1027
- 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/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -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 +158 -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/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- 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 +38 -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 +230 -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/government-walk.js +3 -0
- 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 +7 -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/access_holders.mjs +214 -0
- 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_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- 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
|
@@ -34,6 +34,7 @@ const counts = require('../counts');
|
|
|
34
34
|
const asks = require('../asks');
|
|
35
35
|
const asksDb = require('../asks-db');
|
|
36
36
|
const routing = require('../routing');
|
|
37
|
+
const { accessHandlers } = require('../access-routes');
|
|
37
38
|
const governmentConfig = require('../config');
|
|
38
39
|
const membershipPredicate = require('../membership-predicate');
|
|
39
40
|
const positions = require('../positions');
|
|
@@ -689,6 +690,14 @@ module.exports = function buildGovernmentRouter() {
|
|
|
689
690
|
}
|
|
690
691
|
});
|
|
691
692
|
|
|
693
|
+
// ACCESS HOLDERS (task 1005000): who holds the keys outside Bongos, so an ask can wait
|
|
694
|
+
// on access:<key>. Read on the docket's atom; recorded by the owner. A record of who to
|
|
695
|
+
// ask, never authority (access.js). Bodies: access-routes.js.
|
|
696
|
+
const accessRoutes = accessHandlers({ db, log, parseId, validateOrRespond });
|
|
697
|
+
router.get('/government/access', api.requireBuilder, api.requirePermission('board.vote.cast'), accessRoutes.read);
|
|
698
|
+
router.post('/government/access/:accessKey/holders', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.grant);
|
|
699
|
+
router.delete('/government/access/:accessKey/holders/:builderId', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.revoke);
|
|
700
|
+
|
|
692
701
|
// rank: perm board.vote.cast — the Board Room view: open items with their
|
|
693
702
|
// clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). Gated on
|
|
694
703
|
// the XENOS-floored vote atom, deliberately: seeing the ballot is part of
|
|
@@ -767,6 +776,28 @@ module.exports = function buildGovernmentRouter() {
|
|
|
767
776
|
// treasury; `rule` ∈ decide first consent majority unanimous signoff rule (or
|
|
768
777
|
// null); `state` ∈ needs moving settled. Nothing is ever sample data: a source
|
|
769
778
|
// with nothing recorded yet is `coming`, never a placeholder row.
|
|
779
|
+
// OPEN BLOCKERS UNDER PEOPLE (task 1004999): every open blocker grouped under each
|
|
780
|
+
// person its seat resolves to (routing.js, the City's own rule), and the viewer's own
|
|
781
|
+
// group. The docket's atom: it floors at xenos, so every builder can see whose a
|
|
782
|
+
// blocker is; resolving one keeps its own blocker.review gate.
|
|
783
|
+
router.get('/government/blockers/people', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
784
|
+
try {
|
|
785
|
+
res.json(await docket.blockersByPerson(req.builder));
|
|
786
|
+
} catch (err) {
|
|
787
|
+
log.error('[gds] GET /government/blockers/people', err);
|
|
788
|
+
res.fail('blockers_people_read_failed', { status: 500, message: 'internal error' });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
router.get('/government/blockers/for-me', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
792
|
+
try {
|
|
793
|
+
const r = await docket.blockersByPerson(req.builder);
|
|
794
|
+
res.json({ available: r.available, reason: r.reason || null, ...r.yours });
|
|
795
|
+
} catch (err) {
|
|
796
|
+
log.error('[gds] GET /government/blockers/for-me', err);
|
|
797
|
+
res.fail('blockers_for_me_read_failed', { status: 500, message: 'internal error' });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
770
801
|
router.get('/government/docket', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
771
802
|
try {
|
|
772
803
|
res.json(await docket.docketFor(req.builder));
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// request, uncached (ADR 0016), and routes every matter with routeAll.
|
|
39
39
|
|
|
40
40
|
const { answerFor } = require('./leads');
|
|
41
|
+
const access = require('./access');
|
|
41
42
|
const asks = require('./asks');
|
|
42
43
|
|
|
43
44
|
const BUILDING_NAMES = Object.freeze({
|
|
@@ -59,6 +60,7 @@ function seatOf(matter) {
|
|
|
59
60
|
function seatLabel(seat) {
|
|
60
61
|
const s = String(seat || '');
|
|
61
62
|
if (s === 'owner') return 'the owner';
|
|
63
|
+
if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
|
|
62
64
|
const m = /^(lead|board):([a-z]+)$/.exec(s);
|
|
63
65
|
if (m) return `the ${BUILDING_NAMES[m[2]] || m[2]} ${m[1] === 'lead' ? 'Lead' : 'board'}`;
|
|
64
66
|
return asks.seatWords(s);
|
|
@@ -69,12 +71,42 @@ const idsOf = (list) => (Array.isArray(list) ? list : [])
|
|
|
69
71
|
.filter((id) => id != null)
|
|
70
72
|
.map(String);
|
|
71
73
|
|
|
74
|
+
// Who each builder id is, from the seats already read (task 1004999): the owner
|
|
75
|
+
// seat's Archons, the Leads in office and each building's holders. A route names
|
|
76
|
+
// people, not just a seat — "waits on Lars, the owner", not "waits on the owner".
|
|
77
|
+
// Pure; an id no seat row names reads as { id, login: null, name: null }.
|
|
78
|
+
function directoryOf(ctx) {
|
|
79
|
+
const c = ctx || {};
|
|
80
|
+
const dir = new Map();
|
|
81
|
+
const put = (id, login, name) => {
|
|
82
|
+
if (id == null) return;
|
|
83
|
+
const key = String(id);
|
|
84
|
+
const prev = dir.get(key);
|
|
85
|
+
if (prev && prev.name) return;
|
|
86
|
+
dir.set(key, { id: key, login: login || null, name: name || login || null });
|
|
87
|
+
};
|
|
88
|
+
for (const b of (c.owner && c.owner.builders) || []) put(b.id, b.login, b.name);
|
|
89
|
+
for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
|
|
90
|
+
if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
|
|
91
|
+
}
|
|
92
|
+
for (const list of [...Object.values(c.holders || {}), ...Object.values(c.access || {})]) {
|
|
93
|
+
for (const p of Array.isArray(list) ? list : []) {
|
|
94
|
+
if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return dir;
|
|
98
|
+
}
|
|
99
|
+
function peopleOf(ids, dir) {
|
|
100
|
+
return (ids || []).map((id) => dir.get(String(id)) || { id: String(id), login: null, name: null });
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
// resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
|
|
73
104
|
// asked the seat the matter names
|
|
74
105
|
// seat the seat that answers it, after any fall
|
|
75
106
|
// builder_ids who sits there now ([] when nobody does)
|
|
76
107
|
// ctx { leads: Lead rows in office, owner: leads ownerOf(archons) shape
|
|
77
|
-
// { builders: [{ id }] }, holders: { building: [people] | null }
|
|
108
|
+
// { builders: [{ id }] }, holders: { building: [people] | null },
|
|
109
|
+
// access: { key: [holder rows] } (task 1005000) }
|
|
78
110
|
function resolveSeat(asked, ctx) {
|
|
79
111
|
const c = ctx || {};
|
|
80
112
|
const owner = c.owner || { builders: [] };
|
|
@@ -91,17 +123,24 @@ function resolveSeat(asked, ctx) {
|
|
|
91
123
|
const held = idsOf((c.holders || {})[m[1]]);
|
|
92
124
|
return held.length ? seated(asked, held) : toOwner();
|
|
93
125
|
}
|
|
126
|
+
// task 1005000: an outside access — its recorded holders; nobody recorded → the owner.
|
|
127
|
+
if ((m = /^access:([a-z-]+)$/.exec(String(asked || ''))) && access.isAccessKey(m[1])) {
|
|
128
|
+
const held = idsOf((c.access || {})[m[1]]);
|
|
129
|
+
return held.length ? seated(asked, held) : toOwner();
|
|
130
|
+
}
|
|
94
131
|
if ((m = /^committee:([a-z]+)\.[a-z0-9_]+$/.exec(String(asked || '')))) {
|
|
95
132
|
return { ...resolveSeat(`lead:${m[1]}`, c), asked };
|
|
96
133
|
}
|
|
97
134
|
return toOwner();
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
// routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
|
|
101
|
-
// { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
|
|
137
|
+
// routeMatter(matter, viewerId, ctx, now, dir) → the route the docket carries:
|
|
138
|
+
// { seat, label, asked, fell_to_owner, people, yours, nobody: null | { why, outcome } }
|
|
102
139
|
// `yours` is the one answer to "is this on me": an open matter that needs a
|
|
103
140
|
// person, on a seat the viewer sits in, that somebody can still answer.
|
|
104
|
-
|
|
141
|
+
// `dir` is directoryOf(ctx); routeAll builds it ONCE per request and passes it,
|
|
142
|
+
// so a docket of N matters does not rebuild the same name map N times.
|
|
143
|
+
function routeMatter(matter, viewerId, ctx, now = Date.now(), dir = directoryOf(ctx)) {
|
|
105
144
|
const m = matter || {};
|
|
106
145
|
const needs = m.state === 'needs';
|
|
107
146
|
// a sitting's voters are the item's own (its snapshot), never a fall to the owner
|
|
@@ -120,6 +159,8 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
120
159
|
label: r.label,
|
|
121
160
|
asked: r.asked,
|
|
122
161
|
fell_to_owner: r.fell_to_owner,
|
|
162
|
+
// who sits in that seat now, by name (task 1004999); [] for a sitting's own voters
|
|
163
|
+
people: peopleOf(r.builder_ids, dir),
|
|
123
164
|
yours: needs && !nobody && !!viewer && sits,
|
|
124
165
|
nobody,
|
|
125
166
|
};
|
|
@@ -128,8 +169,9 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
128
169
|
// Route every matter in place; → how many are the viewer's.
|
|
129
170
|
function routeAll(matters, viewerId, ctx, now = Date.now()) {
|
|
130
171
|
let yours = 0;
|
|
172
|
+
const dir = directoryOf(ctx);
|
|
131
173
|
for (const m of matters || []) {
|
|
132
|
-
m.route = routeMatter(m, viewerId, ctx, now);
|
|
174
|
+
m.route = routeMatter(m, viewerId, ctx, now, dir);
|
|
133
175
|
if (m.route.yours) yours += 1;
|
|
134
176
|
}
|
|
135
177
|
return yours;
|
|
@@ -144,11 +186,11 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
|
|
|
144
186
|
if (!m.ask) return { code: 'no_ask', status: 409, message: 'This matter has no ask to pass on yet.' };
|
|
145
187
|
if (m.source === 'board') return { code: 'vote_not_passable', status: 409, message: 'A vote is its voters\' to answer; it cannot be passed on.' };
|
|
146
188
|
const to = String(toSeat || '').trim();
|
|
147
|
-
if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name> or
|
|
189
|
+
if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.' };
|
|
148
190
|
const route = routeMatter(m, viewerId, ctx, now);
|
|
149
191
|
if (!route.yours) return { code: 'not_yours', status: 403, message: 'Only whoever this waits on can pass it on.' };
|
|
150
192
|
if (to === m.ask.waits_on || resolveSeat(to, ctx).seat === route.seat) return { code: 'same_seat', status: 409, message: 'It already waits on that seat.' };
|
|
151
193
|
return null;
|
|
152
194
|
}
|
|
153
195
|
|
|
154
|
-
module.exports = { seatOf, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
196
|
+
module.exports = { seatOf, resolveSeat, directoryOf, peopleOf, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/waiting.js — open blockers grouped UNDER THE PEOPLE they wait on
|
|
4
|
+
// (task 1004999, goal 1000111).
|
|
5
|
+
//
|
|
6
|
+
// WHY. A blocker has no person on it: the blockers table has no assignee, and the only
|
|
7
|
+
// "who" is the seat its ask names (owner | lead:<b> | board:<b> | committee:…). So every
|
|
8
|
+
// surface listed every blocker to everyone, /blocker-review framed them all as the
|
|
9
|
+
// owner's, and nobody could open one place and see "these wait on me" — or see that a
|
|
10
|
+
// blocker was waiting on Lars rather than on "the owner".
|
|
11
|
+
//
|
|
12
|
+
// THE RULE. The seat is the truth; the person is resolved at READ time with the same
|
|
13
|
+
// routing the City uses (routing.js seatOf → resolveSeat), so the two can never
|
|
14
|
+
// disagree about whose a blocker is, and seating a new Lead moves their blockers with
|
|
15
|
+
// no blocker edited. Never copy a name onto a blocker.
|
|
16
|
+
//
|
|
17
|
+
// A blocker waits on EVERY person its seat resolves to (a board's members, or two
|
|
18
|
+
// Archons as the owner), so it is listed under each of them. One that resolves to
|
|
19
|
+
// nobody is listed under `nobody`, never dropped.
|
|
20
|
+
//
|
|
21
|
+
// PURE: no db. docket.js reads the blockers, their asks and the seats, then calls this.
|
|
22
|
+
|
|
23
|
+
const routing = require('./routing');
|
|
24
|
+
const access = require('./access');
|
|
25
|
+
|
|
26
|
+
// One blocker as a line under a person: what it is, where it lives, and the seat it
|
|
27
|
+
// reaches that person through, in words ("as the owner", "as the Planning Lead").
|
|
28
|
+
function itemOf(m, r) {
|
|
29
|
+
return {
|
|
30
|
+
id: m.ref && m.ref.id != null ? String(m.ref.id) : String(m.id),
|
|
31
|
+
matter_id: String(m.id),
|
|
32
|
+
title: m.title,
|
|
33
|
+
href: m.href || null,
|
|
34
|
+
opened_at: m.opened_at || null,
|
|
35
|
+
where: m.where || null,
|
|
36
|
+
seat: r.seat,
|
|
37
|
+
as: r.label,
|
|
38
|
+
asked: r.asked,
|
|
39
|
+
fell_to_owner: !!r.fell_to_owner,
|
|
40
|
+
// task 1005000: a blocker only an outside-access holder can clear reads as its own
|
|
41
|
+
// category ("needs repo-owner access"), not as one more of the owner's.
|
|
42
|
+
needs: /^access:/.test(String(r.asked || '')) ? access.needsWords(String(r.asked).slice(7)) : null,
|
|
43
|
+
question: m.ask && m.ask.question ? String(m.ask.question) : null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const byOpened = (a, b) => String(a.opened_at || '').localeCompare(String(b.opened_at || '')) || a.id.localeCompare(b.id, 'en', { numeric: true });
|
|
48
|
+
|
|
49
|
+
// groupByPerson(matters, seats) → { people: [{ person, count, items }], nobody: [items] }
|
|
50
|
+
// People with the most waiting first, then by name; inside a person, oldest first.
|
|
51
|
+
function groupByPerson(matters, seats) {
|
|
52
|
+
const dir = routing.directoryOf(seats);
|
|
53
|
+
const groups = new Map();
|
|
54
|
+
const nobody = [];
|
|
55
|
+
for (const m of matters || []) {
|
|
56
|
+
if (!m || m.state === 'settled') continue;
|
|
57
|
+
const r = routing.resolveSeat(routing.seatOf(m), seats);
|
|
58
|
+
const item = itemOf(m, r);
|
|
59
|
+
if (!r.builder_ids.length) { nobody.push(item); continue; }
|
|
60
|
+
for (const p of routing.peopleOf(r.builder_ids, dir)) {
|
|
61
|
+
if (!groups.has(p.id)) groups.set(p.id, { person: p, items: [] });
|
|
62
|
+
groups.get(p.id).items.push(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const people = [...groups.values()]
|
|
66
|
+
.map((g) => ({ person: g.person, count: g.items.length, items: g.items.sort(byOpened) }))
|
|
67
|
+
.sort((a, b) => b.count - a.count || String(a.person.name || a.person.id).localeCompare(String(b.person.name || b.person.id)));
|
|
68
|
+
return { people, nobody: nobody.sort(byOpened) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The viewer's own group: what waits on THEM. → { person, count, items } (count 0 when none).
|
|
72
|
+
function forViewer(grouped, viewerId) {
|
|
73
|
+
const id = viewerId == null ? null : String(viewerId);
|
|
74
|
+
const g = id && grouped && Array.isArray(grouped.people) ? grouped.people.find((x) => x.person.id === id) : null;
|
|
75
|
+
return g || { person: id ? { id, login: null, name: null } : null, count: 0, items: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { groupByPerson, forViewer };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// modules/hall-ui/public/ai-studio-lib.js — the pure rules of the Studio's AI
|
|
2
|
+
// pages, /builders/ai-studio (task 1004921, goal 1000136, ADR 0392).
|
|
3
|
+
//
|
|
4
|
+
// Built 1:1 on the owner's pick, option C of the AI Studio options canvas
|
|
5
|
+
// ("Three steps: pick, describe, make"), with the owner's addition that the
|
|
6
|
+
// results page lets the request be changed and made again. Everything here is
|
|
7
|
+
// pure: money in words, the receipt, the month's summary of the ledger, the
|
|
8
|
+
// waiting checklist, which view a URL names, and the reply's safe markup.
|
|
9
|
+
// ai-studio.js does the reads, the writes and the painting.
|
|
10
|
+
//
|
|
11
|
+
// Money is integer micro-dollars everywhere (1,000,000 = $1), as the server
|
|
12
|
+
// sends it. A PRICE with sub-cent digits shows four decimals ($0.0421); a
|
|
13
|
+
// whole-cent amount and a BALANCE show two ($12.40). A balance rounds DOWN, so
|
|
14
|
+
// the page never promises a cent that is not there.
|
|
15
|
+
//
|
|
16
|
+
// The file limits below are the upload checks' (modules/ai-studio/studio.js),
|
|
17
|
+
// repeated so the page can say no before a 4 MB upload starts. The server
|
|
18
|
+
// still checks every file; these only spare the wait.
|
|
19
|
+
//
|
|
20
|
+
// Node-require-safe: assignment to window is the only side effect, and only
|
|
21
|
+
// when a window exists (tests/hall_ai_studio.mjs requires it directly).
|
|
22
|
+
|
|
23
|
+
(function (root) {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const MICRO_PER_USD = 1000000;
|
|
27
|
+
const MICRO_PER_CENT = 10000;
|
|
28
|
+
const MAX_FILES = 6;
|
|
29
|
+
const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
const MAX_PDF_BYTES = 8 * 1024 * 1024;
|
|
31
|
+
const IMAGE_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
|
|
32
|
+
const PDF_TYPE = 'application/pdf';
|
|
33
|
+
const FINISHED = Object.freeze(['done', 'failed', 'refused']);
|
|
34
|
+
const STEPS = Object.freeze(['pick', 'describe', 'make']);
|
|
35
|
+
const TOPUP_CHOICES = Object.freeze([5, 10, 20, 50]);
|
|
36
|
+
// The waiting checklist is paced by the clock: the server reports a request
|
|
37
|
+
// as queued or running, not which call it is on, so "wrote the answer" is
|
|
38
|
+
// ticked once a run has gone on long enough for the reply to be back.
|
|
39
|
+
const ANSWER_AFTER_SEC = 25;
|
|
40
|
+
|
|
41
|
+
function isMicro(v) { return Number.isSafeInteger(v); }
|
|
42
|
+
|
|
43
|
+
// "$0.0421", "$0.50", "$12.40". Sub-cent amounts keep four decimals.
|
|
44
|
+
function usd(micro) {
|
|
45
|
+
if (!isMicro(micro)) return '';
|
|
46
|
+
const neg = micro < 0;
|
|
47
|
+
const a = Math.abs(micro);
|
|
48
|
+
const text = a % MICRO_PER_CENT === 0
|
|
49
|
+
? (a / MICRO_PER_USD).toFixed(2)
|
|
50
|
+
: (Math.round(a / 100) / 10000).toFixed(4);
|
|
51
|
+
return `${neg ? '−' : ''}$${text}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A balance: two decimals, rounded down.
|
|
55
|
+
function balanceUsd(micro) {
|
|
56
|
+
if (!isMicro(micro)) return '';
|
|
57
|
+
const cents = Math.floor(micro / MICRO_PER_CENT);
|
|
58
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sizeText(bytes) {
|
|
62
|
+
const n = Number(bytes) || 0;
|
|
63
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
64
|
+
if (n >= 1024) return `${Math.round(n / 1024)} KB`;
|
|
65
|
+
return `${n} B`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Which page the address names. ?request=N is the waiting page or the
|
|
69
|
+
// result; ?view=balance the balance; otherwise one of the three steps.
|
|
70
|
+
function routeOf(search) {
|
|
71
|
+
const q = new URLSearchParams(String(search || ''));
|
|
72
|
+
const request = q.get('request');
|
|
73
|
+
if (request && /^\d{1,18}$/.test(request)) return { view: 'request', id: request };
|
|
74
|
+
if (q.get('view') === 'balance') return { view: 'balance', all: q.get('all') === '1' };
|
|
75
|
+
const step = STEPS.includes(q.get('step')) ? q.get('step') : 'pick';
|
|
76
|
+
const from = q.get('from');
|
|
77
|
+
return { view: 'step', step, from: from && /^\d{1,18}$/.test(from) ? from : null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Says no to a file before it is sent, in the same words the server would.
|
|
81
|
+
function checkFile(file, count) {
|
|
82
|
+
if (count >= MAX_FILES) return `Send up to ${MAX_FILES} files at a time.`;
|
|
83
|
+
const type = String(file && file.type || '');
|
|
84
|
+
if (type !== PDF_TYPE && !IMAGE_TYPES.includes(type)) return 'Pictures (PNG, JPG, GIF, WebP) or a PDF only.';
|
|
85
|
+
const max = type === PDF_TYPE ? MAX_PDF_BYTES : MAX_IMAGE_BYTES;
|
|
86
|
+
if ((Number(file.size) || 0) > max) return type === PDF_TYPE ? 'PDFs up to 8 MB.' : 'Pictures up to 4 MB.';
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isImage(contentType) { return IMAGE_TYPES.includes(String(contentType || '')); }
|
|
91
|
+
|
|
92
|
+
// The step header: "1 Pick ✓", "2 Describe", "3 Make".
|
|
93
|
+
function stepMarks(step) {
|
|
94
|
+
const at = STEPS.indexOf(step);
|
|
95
|
+
return [['1', 'Pick'], ['2', 'Describe'], ['3', 'Make']].map(([n, label], i) => ({
|
|
96
|
+
text: `${n} ${label}${i < at ? ' ✓' : ''}`,
|
|
97
|
+
state: i < at ? 'done' : (i === at ? 'now' : 'later'),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The prompt as one quoted line for step 3's summary row.
|
|
102
|
+
function quoteLine(prompt, max) {
|
|
103
|
+
const t = String(prompt || '').replace(/\s+/g, ' ').trim();
|
|
104
|
+
const cap = max || 64;
|
|
105
|
+
return `“${t.length > cap ? `${t.slice(0, cap - 1).trimEnd()}…` : t}”`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The own key the server will run on: OpenRouter first, as keys.chooseKey
|
|
109
|
+
// prefers it (it reports its cost). null when none is saved.
|
|
110
|
+
function ownKey(keys) {
|
|
111
|
+
const list = (keys && keys.keys) || [];
|
|
112
|
+
const set = (p) => list.find((k) => k && k.provider === p && k.set);
|
|
113
|
+
const k = set('openrouter') || set('gemini');
|
|
114
|
+
if (!k) return null;
|
|
115
|
+
return k.provider === 'openrouter'
|
|
116
|
+
? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
|
|
117
|
+
: { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Step 3's two cards. balance is GET /ai-studio/balance's body.
|
|
121
|
+
function payChoices(keys, balance) {
|
|
122
|
+
const own = ownKey(keys);
|
|
123
|
+
const b = balance || {};
|
|
124
|
+
const avail = b.balance ? b.balance.available_micro : null;
|
|
125
|
+
const max = isMicro(b.max_cost_micro) ? b.max_cost_micro : null;
|
|
126
|
+
const payments = !!b.balance;
|
|
127
|
+
return {
|
|
128
|
+
own,
|
|
129
|
+
payments,
|
|
130
|
+
setAside: max === null ? '' : usd(max),
|
|
131
|
+
left: payments ? `${balanceUsd(avail)} left` : '',
|
|
132
|
+
short: payments && max !== null && avail < max,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The waiting page's three lines, from the request's status and how long it
|
|
137
|
+
// has run.
|
|
138
|
+
function waitSteps(status, elapsedSec, fileCount) {
|
|
139
|
+
const n = Number(fileCount) || 0;
|
|
140
|
+
const files = n === 1 ? 'Read your file' : `Read your ${n} files`;
|
|
141
|
+
const running = status === 'running';
|
|
142
|
+
const answered = running && elapsedSec >= ANSWER_AFTER_SEC;
|
|
143
|
+
return [
|
|
144
|
+
{ text: files, state: running ? 'done' : 'now' },
|
|
145
|
+
{ text: 'Wrote the answer', state: answered ? 'done' : (running ? 'now' : 'later') },
|
|
146
|
+
{ text: 'Drawing the pictures', state: answered ? 'now' : 'later' },
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function clock(sec) {
|
|
151
|
+
const s = Math.max(0, Math.floor(Number(sec) || 0));
|
|
152
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isFinished(status) { return FINISHED.includes(status); }
|
|
156
|
+
|
|
157
|
+
// The receipt on the results page: rows of [label, value] and a note.
|
|
158
|
+
// (The stored request does not name which of the builder's keys ran.)
|
|
159
|
+
// key_source 'ours' is the balance; 'own' with a reported cost is an
|
|
160
|
+
// OpenRouter key; 'own' with none is a Gemini key (Google reports no price).
|
|
161
|
+
// A request that did not finish has no receipt: its error line already says
|
|
162
|
+
// what was (not) charged.
|
|
163
|
+
function receipt(req, multiplier) {
|
|
164
|
+
const r = req || {};
|
|
165
|
+
if (r.status !== 'done') return { rows: [], note: '' };
|
|
166
|
+
if (r.key_source === 'ours' && isMicro(r.price_micro)) {
|
|
167
|
+
const pct = Number(multiplier) > 1 ? Math.round((Number(multiplier) - 1) * 100) : null;
|
|
168
|
+
return {
|
|
169
|
+
rows: [
|
|
170
|
+
['Paid with', 'your balance'],
|
|
171
|
+
['OpenRouter', usd(r.provider_cost_micro)],
|
|
172
|
+
[pct === null ? 'Ours' : `Our ${pct}%`, usd(r.markup_micro)],
|
|
173
|
+
['You paid', usd(r.price_micro)],
|
|
174
|
+
],
|
|
175
|
+
note: '',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (r.key_source === 'own' && isMicro(r.provider_cost_micro)) {
|
|
179
|
+
return {
|
|
180
|
+
rows: [['Paid with', 'your own OpenRouter key'], ['Cost', usd(r.provider_cost_micro)]],
|
|
181
|
+
note: 'OpenRouter bills you for it directly. We charged nothing.',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (r.key_source === 'own') {
|
|
185
|
+
return {
|
|
186
|
+
rows: [['Paid with', 'your own Gemini key'], ['Cost', 'not reported']],
|
|
187
|
+
note: "Google doesn't tell us the price on your own key, so check your Google bill. We charged nothing.",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return { rows: [], note: '' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// "Same 3 files, paid the same way."
|
|
194
|
+
function againLine(uploadCount) {
|
|
195
|
+
const n = Number(uploadCount) || 0;
|
|
196
|
+
return `Change the words and make it again. Same ${n === 1 ? 'file' : `${n} files`}, paid the same way.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The month's summary of the ledger rows that fall in `now`'s month.
|
|
200
|
+
function monthSummary(ledger, now) {
|
|
201
|
+
const d = now instanceof Date ? now : new Date();
|
|
202
|
+
const inMonth = (row) => {
|
|
203
|
+
const t = new Date(row.created_at);
|
|
204
|
+
return !Number.isNaN(t.getTime()) && t.getFullYear() === d.getFullYear() && t.getMonth() === d.getMonth();
|
|
205
|
+
};
|
|
206
|
+
const rows = (ledger || []).filter(inMonth);
|
|
207
|
+
const sum = (kind) => rows.filter((r) => r.kind === kind).reduce((a, r) => a + (Number(r.amount_micro) || 0), 0);
|
|
208
|
+
const count = (kind) => rows.filter((r) => r.kind === kind).length;
|
|
209
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
210
|
+
const lines = [];
|
|
211
|
+
if (count('topup')) lines.push({ label: 'Added', value: `+${usd(sum('topup'))}`, tone: 'ok' });
|
|
212
|
+
if (count('capture')) lines.push({ label: `Spent on ${plural(count('capture'), 'request', 'requests')}`, value: usd(-sum('capture')), tone: '' });
|
|
213
|
+
if (count('release')) lines.push({ label: `Money back from ${plural(count('release'), 'failed request', 'failed requests')}`, value: `${usd(sum('release'))} returned`, tone: 'ok' });
|
|
214
|
+
if (count('own_key')) lines.push({ label: `On your own keys, ${plural(count('own_key'), 'request', 'requests')}`, value: 'free', tone: 'dim' });
|
|
215
|
+
return { month: d.toLocaleString('en', { month: 'long' }), lines };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// One line of the full ledger. `prompts` maps request id to its prompt.
|
|
219
|
+
function ledgerLine(row, prompts) {
|
|
220
|
+
const r = row || {};
|
|
221
|
+
const what = (r.request_id && prompts && prompts[String(r.request_id)])
|
|
222
|
+
? quoteLine(prompts[String(r.request_id)], 40).slice(1, -1)
|
|
223
|
+
: 'A request';
|
|
224
|
+
const amt = Number(r.amount_micro) || 0;
|
|
225
|
+
switch (r.kind) {
|
|
226
|
+
case 'topup': return { what: 'Top up', detail: String(r.note || '').replace(/^card charged /, 'card '), value: `+${usd(amt)}`, tone: 'ok' };
|
|
227
|
+
case 'hold': return { what, detail: 'set aside while it ran', value: usd(amt), tone: 'dim' };
|
|
228
|
+
case 'capture': return { what, detail: `${usd(Number(r.provider_cost_micro) || 0)} + ${usd(Number(r.markup_micro) || 0)}`, value: usd(-amt), tone: '' };
|
|
229
|
+
case 'release': return { what, detail: 'failed, all money back', value: `${usd(amt)} returned`, tone: 'ok' };
|
|
230
|
+
case 'own_key': return { what, detail: 'your own key', value: 'free', tone: 'dim' };
|
|
231
|
+
default: return { what, detail: '', value: usd(amt), tone: '' };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The model's reply as paragraphs, **bold** kept, everything else escaped by
|
|
236
|
+
// the hall's ONE escaper, handed in (window.OTB.escapeHtml in the page).
|
|
237
|
+
function replyHtml(md, escape) {
|
|
238
|
+
const text = String(md || '').replace(/\r\n/g, '\n').trim();
|
|
239
|
+
if (!text || typeof escape !== 'function') return '';
|
|
240
|
+
return text.split(/\n{2,}/).map((para) => {
|
|
241
|
+
const safe = escape(para.trim()).replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>');
|
|
242
|
+
return `<p>${safe}</p>`;
|
|
243
|
+
}).join('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The home's AI action line: the newest request, while it is being made or
|
|
247
|
+
// when it finished in the last day, is what the line points at.
|
|
248
|
+
function homeLine(requests, now) {
|
|
249
|
+
const newest = (requests || [])[0];
|
|
250
|
+
const t = now instanceof Date ? now.getTime() : Date.now();
|
|
251
|
+
if (newest && (newest.status === 'queued' || newest.status === 'running')) {
|
|
252
|
+
return { detail: 'one is being made now', href: `/builders/ai-studio?request=${newest.id}` };
|
|
253
|
+
}
|
|
254
|
+
const fin = newest && newest.finished_at ? new Date(newest.finished_at).getTime() : NaN;
|
|
255
|
+
if (newest && newest.status === 'done' && t - fin < 24 * 3600 * 1000) {
|
|
256
|
+
return { detail: 'your newest pictures are ready', href: `/builders/ai-studio?request=${newest.id}` };
|
|
257
|
+
}
|
|
258
|
+
return { detail: 'sketches in, ideas and pictures out', href: '/builders/ai-studio' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const lib = {
|
|
262
|
+
MAX_FILES, MAX_IMAGE_BYTES, MAX_PDF_BYTES, IMAGE_TYPES, PDF_TYPE, STEPS, TOPUP_CHOICES, ANSWER_AFTER_SEC,
|
|
263
|
+
usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, payChoices,
|
|
264
|
+
waitSteps, clock, isFinished, receipt, againLine, monthSummary, ledgerLine, replyHtml, homeLine,
|
|
265
|
+
};
|
|
266
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = lib;
|
|
267
|
+
if (root) root.OTBAiStudioLib = lib;
|
|
268
|
+
})(typeof window !== 'undefined' ? window : null);
|