@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
|
@@ -69,6 +69,35 @@ const idsOf = (list) => (Array.isArray(list) ? list : [])
|
|
|
69
69
|
.filter((id) => id != null)
|
|
70
70
|
.map(String);
|
|
71
71
|
|
|
72
|
+
// Who each builder id is, from the seats already read (task 1004999): the owner
|
|
73
|
+
// seat's Archons, the Leads in office and each building's holders. A route names
|
|
74
|
+
// people, not just a seat — "waits on Lars, the owner", not "waits on the owner".
|
|
75
|
+
// Pure; an id no seat row names reads as { id, login: null, name: null }.
|
|
76
|
+
function directoryOf(ctx) {
|
|
77
|
+
const c = ctx || {};
|
|
78
|
+
const dir = new Map();
|
|
79
|
+
const put = (id, login, name) => {
|
|
80
|
+
if (id == null) return;
|
|
81
|
+
const key = String(id);
|
|
82
|
+
const prev = dir.get(key);
|
|
83
|
+
if (prev && prev.name) return;
|
|
84
|
+
dir.set(key, { id: key, login: login || null, name: name || login || null });
|
|
85
|
+
};
|
|
86
|
+
for (const b of (c.owner && c.owner.builders) || []) put(b.id, b.login, b.name);
|
|
87
|
+
for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
|
|
88
|
+
if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
|
|
89
|
+
}
|
|
90
|
+
for (const list of Object.values(c.holders || {})) {
|
|
91
|
+
for (const p of Array.isArray(list) ? list : []) {
|
|
92
|
+
if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return dir;
|
|
96
|
+
}
|
|
97
|
+
function peopleOf(ids, dir) {
|
|
98
|
+
return (ids || []).map((id) => dir.get(String(id)) || { id: String(id), login: null, name: null });
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
|
|
73
102
|
// asked the seat the matter names
|
|
74
103
|
// seat the seat that answers it, after any fall
|
|
@@ -97,11 +126,13 @@ function resolveSeat(asked, ctx) {
|
|
|
97
126
|
return toOwner();
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
// routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
|
|
101
|
-
// { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
|
|
129
|
+
// routeMatter(matter, viewerId, ctx, now, dir) → the route the docket carries:
|
|
130
|
+
// { seat, label, asked, fell_to_owner, people, yours, nobody: null | { why, outcome } }
|
|
102
131
|
// `yours` is the one answer to "is this on me": an open matter that needs a
|
|
103
132
|
// person, on a seat the viewer sits in, that somebody can still answer.
|
|
104
|
-
|
|
133
|
+
// `dir` is directoryOf(ctx); routeAll builds it ONCE per request and passes it,
|
|
134
|
+
// so a docket of N matters does not rebuild the same name map N times.
|
|
135
|
+
function routeMatter(matter, viewerId, ctx, now = Date.now(), dir = directoryOf(ctx)) {
|
|
105
136
|
const m = matter || {};
|
|
106
137
|
const needs = m.state === 'needs';
|
|
107
138
|
// a sitting's voters are the item's own (its snapshot), never a fall to the owner
|
|
@@ -120,6 +151,8 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
120
151
|
label: r.label,
|
|
121
152
|
asked: r.asked,
|
|
122
153
|
fell_to_owner: r.fell_to_owner,
|
|
154
|
+
// who sits in that seat now, by name (task 1004999); [] for a sitting's own voters
|
|
155
|
+
people: peopleOf(r.builder_ids, dir),
|
|
123
156
|
yours: needs && !nobody && !!viewer && sits,
|
|
124
157
|
nobody,
|
|
125
158
|
};
|
|
@@ -128,8 +161,9 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
128
161
|
// Route every matter in place; → how many are the viewer's.
|
|
129
162
|
function routeAll(matters, viewerId, ctx, now = Date.now()) {
|
|
130
163
|
let yours = 0;
|
|
164
|
+
const dir = directoryOf(ctx);
|
|
131
165
|
for (const m of matters || []) {
|
|
132
|
-
m.route = routeMatter(m, viewerId, ctx, now);
|
|
166
|
+
m.route = routeMatter(m, viewerId, ctx, now, dir);
|
|
133
167
|
if (m.route.yours) yours += 1;
|
|
134
168
|
}
|
|
135
169
|
return yours;
|
|
@@ -151,4 +185,4 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
|
|
|
151
185
|
return null;
|
|
152
186
|
}
|
|
153
187
|
|
|
154
|
-
module.exports = { seatOf, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
188
|
+
module.exports = { seatOf, resolveSeat, directoryOf, peopleOf, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
25
|
+
// One blocker as a line under a person: what it is, where it lives, and the seat it
|
|
26
|
+
// reaches that person through, in words ("as the owner", "as the Planning Lead").
|
|
27
|
+
function itemOf(m, r) {
|
|
28
|
+
return {
|
|
29
|
+
id: m.ref && m.ref.id != null ? String(m.ref.id) : String(m.id),
|
|
30
|
+
matter_id: String(m.id),
|
|
31
|
+
title: m.title,
|
|
32
|
+
href: m.href || null,
|
|
33
|
+
opened_at: m.opened_at || null,
|
|
34
|
+
where: m.where || null,
|
|
35
|
+
seat: r.seat,
|
|
36
|
+
as: r.label,
|
|
37
|
+
asked: r.asked,
|
|
38
|
+
fell_to_owner: !!r.fell_to_owner,
|
|
39
|
+
question: m.ask && m.ask.question ? String(m.ask.question) : null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const byOpened = (a, b) => String(a.opened_at || '').localeCompare(String(b.opened_at || '')) || a.id.localeCompare(b.id, 'en', { numeric: true });
|
|
44
|
+
|
|
45
|
+
// groupByPerson(matters, seats) → { people: [{ person, count, items }], nobody: [items] }
|
|
46
|
+
// People with the most waiting first, then by name; inside a person, oldest first.
|
|
47
|
+
function groupByPerson(matters, seats) {
|
|
48
|
+
const dir = routing.directoryOf(seats);
|
|
49
|
+
const groups = new Map();
|
|
50
|
+
const nobody = [];
|
|
51
|
+
for (const m of matters || []) {
|
|
52
|
+
if (!m || m.state === 'settled') continue;
|
|
53
|
+
const r = routing.resolveSeat(routing.seatOf(m), seats);
|
|
54
|
+
const item = itemOf(m, r);
|
|
55
|
+
if (!r.builder_ids.length) { nobody.push(item); continue; }
|
|
56
|
+
for (const p of routing.peopleOf(r.builder_ids, dir)) {
|
|
57
|
+
if (!groups.has(p.id)) groups.set(p.id, { person: p, items: [] });
|
|
58
|
+
groups.get(p.id).items.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const people = [...groups.values()]
|
|
62
|
+
.map((g) => ({ person: g.person, count: g.items.length, items: g.items.sort(byOpened) }))
|
|
63
|
+
.sort((a, b) => b.count - a.count || String(a.person.name || a.person.id).localeCompare(String(b.person.name || b.person.id)));
|
|
64
|
+
return { people, nobody: nobody.sort(byOpened) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The viewer's own group: what waits on THEM. → { person, count, items } (count 0 when none).
|
|
68
|
+
function forViewer(grouped, viewerId) {
|
|
69
|
+
const id = viewerId == null ? null : String(viewerId);
|
|
70
|
+
const g = id && grouped && Array.isArray(grouped.people) ? grouped.people.find((x) => x.person.id === id) : null;
|
|
71
|
+
return g || { person: id ? { id, login: null, name: null } : null, count: 0, items: [] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { groupByPerson, forViewer };
|
|
@@ -183,18 +183,23 @@ function runSubagent({ prompt, opts = {} }) {
|
|
|
183
183
|
// agentic Read turns re-opening the same changed files. Read them ONCE here
|
|
184
184
|
// and hand every worker the post-change contents inline (fenced, untrusted),
|
|
185
185
|
// so a worker's Read tool is only needed for files beyond the bundle.
|
|
186
|
+
//
|
|
187
|
+
// task 1004932: the bundle rides in EVERY worker prompt, so its total is paid
|
|
188
|
+
// once per worker; 120k chars made it the largest fixed input of a grade.
|
|
186
189
|
const CONTEXT_BUNDLE_MAX_FILES = 24;
|
|
187
|
-
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS =
|
|
190
|
+
const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 60_000;
|
|
188
191
|
const CONTEXT_BUNDLE_MAX_FILE_CHARS = 16_000;
|
|
189
192
|
// Binary-ish extensions never worth inlining (the diff already names them).
|
|
190
193
|
const CONTEXT_BUNDLE_SKIP_RE = /\.(?:png|jpe?g|gif|webp|ico|svgz|woff2?|ttf|eot|zip|gz|tgz|tar|pdf|mp[34]|wav|ogg|bin|exe|dll|node)$/i;
|
|
191
194
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
+
// One pass over a unified diff, per file: how many lines it adds (a relevance
|
|
196
|
+
// weight), and the added lines long enough to be a meaningful fingerprint (a bare
|
|
197
|
+
// `}` or `});` matches anywhere) — used to prove a truncated bundle prefix
|
|
198
|
+
// actually SHOWS the change (task 1002905).
|
|
195
199
|
const BUNDLE_EVIDENCE_MIN_CHARS = 12;
|
|
196
|
-
function
|
|
197
|
-
const
|
|
200
|
+
function scanDiff(diff) {
|
|
201
|
+
const evidence = new Map();
|
|
202
|
+
const added = new Map();
|
|
198
203
|
let cur = null;
|
|
199
204
|
let prevWasOldHeader = false;
|
|
200
205
|
for (const line of String(diff || '').split('\n')) {
|
|
@@ -209,12 +214,26 @@ function diffAddedLinesByFile(diff) {
|
|
|
209
214
|
prevWasOldHeader = line.startsWith('--- ');
|
|
210
215
|
if (prevWasOldHeader || line.startsWith('diff --git')) continue;
|
|
211
216
|
if (!cur || !line.startsWith('+')) continue;
|
|
217
|
+
added.set(cur, (added.get(cur) || 0) + 1);
|
|
212
218
|
const t = line.slice(1).trim();
|
|
213
219
|
if (t.length < BUNDLE_EVIDENCE_MIN_CHARS) continue;
|
|
214
|
-
if (!
|
|
215
|
-
|
|
220
|
+
if (!evidence.has(cur)) evidence.set(cur, []);
|
|
221
|
+
evidence.get(cur).push(t);
|
|
216
222
|
}
|
|
217
|
-
return
|
|
223
|
+
return { evidence, added };
|
|
224
|
+
}
|
|
225
|
+
function diffAddedLinesByFile(diff) { return scanDiff(diff).evidence; }
|
|
226
|
+
|
|
227
|
+
// The order the bundle is filled in (task 1004932): the files the diff adds the
|
|
228
|
+
// most lines to first, so when the total cap bites it is the lightly touched
|
|
229
|
+
// files (a one-line doc pointer, a generated index) that fall to skipped[] for
|
|
230
|
+
// the worker to Read, not the heart of the change. Stable: ties, and every file
|
|
231
|
+
// when there is no diff, keep the caller's order.
|
|
232
|
+
function orderByChange(changedFiles, added) {
|
|
233
|
+
return changedFiles
|
|
234
|
+
.map((file, i) => ({ file, i, n: added.get(file) || 0 }))
|
|
235
|
+
.sort((a, b) => (b.n - a.n) || (a.i - b.i))
|
|
236
|
+
.map((x) => x.file);
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
// Pure-ish (fs reads only, injectable): returns { files: [{file, content,
|
|
@@ -241,8 +260,8 @@ function buildContextBundle({ changedFiles, cwd, diff, fsImpl = fs } = {}) {
|
|
|
241
260
|
// exactly the pre-change state). A prefix that contains none of the change is
|
|
242
261
|
// worse than no bundle at all, so refuse it: skipped[] already tells the worker
|
|
243
262
|
// to Read the file.
|
|
244
|
-
const addedByFile =
|
|
245
|
-
for (const rel of (Array.isArray(changedFiles) ? changedFiles : [])) {
|
|
263
|
+
const { evidence: addedByFile, added } = scanDiff(diff);
|
|
264
|
+
for (const rel of orderByChange(Array.isArray(changedFiles) ? changedFiles : [], added)) {
|
|
246
265
|
if (files.length >= CONTEXT_BUNDLE_MAX_FILES || total >= CONTEXT_BUNDLE_MAX_TOTAL_CHARS) {
|
|
247
266
|
skipped.push(rel);
|
|
248
267
|
continue;
|
|
@@ -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);
|