@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
|
@@ -20,20 +20,38 @@ const express = require('express');
|
|
|
20
20
|
// intra-module require — a module never requires a core internal.
|
|
21
21
|
const api = require('../../../src/module-api');
|
|
22
22
|
const cost = require('../cost');
|
|
23
|
+
const { checkCostWrite, MAX_ROW_USD } = require('../cost-write-guard');
|
|
23
24
|
const { validateOrRespond, LIMITS } = api;
|
|
24
25
|
|
|
25
26
|
const COST_CATEGORIES = ['api', 'compute', 'infra', 'domain', 'art', 'other'];
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// Has this builder ever claimed this task? A released claim still counts: a
|
|
29
|
+
// session's cost lands after its ship frees the claim (task 1004959). Served by
|
|
30
|
+
// idx_claims_task_all (claims (task_id), core_263): a task carries a handful of
|
|
31
|
+
// claims ever, so the builder_id filter runs over those rows, never the table.
|
|
32
|
+
async function builderHasClaimed(taskId, builderId) {
|
|
33
|
+
const { rows } = await api.pool.query(
|
|
34
|
+
'SELECT 1 FROM claims WHERE task_id = $1 AND builder_id = $2 LIMIT 1',
|
|
35
|
+
[taskId, builderId],
|
|
36
|
+
);
|
|
37
|
+
return rows.length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// deps.logCost / deps.hasClaimed are test seams; production uses the real ones.
|
|
41
|
+
module.exports = function buildCostRouter(deps = {}) {
|
|
28
42
|
// The Treasury's floors and record in the Governor City (task 1004527).
|
|
29
43
|
require('../governor-docket').contributeEconomyToDocket();
|
|
30
44
|
const router = express.Router();
|
|
45
|
+
const logCost = deps.logCost || ((...a) => cost.logCost(...a));
|
|
46
|
+
const hasClaimed = deps.hasClaimed || builderHasClaimed;
|
|
31
47
|
|
|
32
48
|
// rank: any-builder — log cost on own claim (idempotent via source/source_ref).
|
|
49
|
+
// Who may book what (reserved sources, the caller's own task, the per-row cap)
|
|
50
|
+
// is cost-write-guard.js — security report 1000029.
|
|
33
51
|
router.post('/cost', api.requireBuilder, async (req, res) => {
|
|
34
52
|
if (validateOrRespond(req, res, {
|
|
35
53
|
task_id: { type: 'integer', min: 1 },
|
|
36
|
-
amount_usd: { required: true, type: 'number', min: 0, max:
|
|
54
|
+
amount_usd: { required: true, type: 'number', min: 0, max: MAX_ROW_USD },
|
|
37
55
|
// Don't enum-restrict category — the existing skills set categories that
|
|
38
56
|
// may legitimately drift; just cap the length. (CLAUDE.md mentions
|
|
39
57
|
// api|compute|infra|domain|art|other as the canonical set.)
|
|
@@ -45,10 +63,10 @@ module.exports = function buildCostRouter() {
|
|
|
45
63
|
// System 1 / task 1344: optional per-tier USD split (cost_log columns
|
|
46
64
|
// added by migration 123). session-cost.js sends these so spend can be
|
|
47
65
|
// attributed by tier (cache_read is the marathon sink). Omitted → NULL.
|
|
48
|
-
fresh_input_usd: { type: 'number', min: 0, max:
|
|
49
|
-
cache_read_usd: { type: 'number', min: 0, max:
|
|
50
|
-
cache_write_usd: { type: 'number', min: 0, max:
|
|
51
|
-
output_usd: { type: 'number', min: 0, max:
|
|
66
|
+
fresh_input_usd: { type: 'number', min: 0, max: MAX_ROW_USD },
|
|
67
|
+
cache_read_usd: { type: 'number', min: 0, max: MAX_ROW_USD },
|
|
68
|
+
cache_write_usd: { type: 'number', min: 0, max: MAX_ROW_USD },
|
|
69
|
+
output_usd: { type: 'number', min: 0, max: MAX_ROW_USD },
|
|
52
70
|
// V3.R11 (#91): builder_id is optional. When absent we default to the
|
|
53
71
|
// authed builder (the right answer 99% of the time). When provided it
|
|
54
72
|
// must match the authed builder — we don't let one builder attribute
|
|
@@ -85,7 +103,15 @@ module.exports = function buildCostRouter() {
|
|
|
85
103
|
recordedAtDate = parsed;
|
|
86
104
|
}
|
|
87
105
|
try {
|
|
88
|
-
const
|
|
106
|
+
const refused = await checkCostWrite({
|
|
107
|
+
source,
|
|
108
|
+
taskId: taskId ?? null,
|
|
109
|
+
rank: req.builder?.rank,
|
|
110
|
+
rankMeets: api.rankMeetsThreshold,
|
|
111
|
+
callerHasClaimed: (id) => hasClaimed(id, authedBuilderId),
|
|
112
|
+
});
|
|
113
|
+
if (refused) return res.fail(refused.code, { status: refused.status, message: refused.message });
|
|
114
|
+
const out = await logCost({
|
|
89
115
|
taskId: taskId ?? null,
|
|
90
116
|
amountUsd,
|
|
91
117
|
category,
|
|
@@ -238,6 +238,12 @@ const PERMISSIONS = [
|
|
|
238
238
|
// Review the store's proposed sub-categories (task 1004355, ADR 0342 D3): approve,
|
|
239
239
|
// rename then approve, or merge into an existing one (a merge moves its modules).
|
|
240
240
|
{ key: 'module.subcategory.review', system: false, floor: 'metic', guards: 'GET /store/subcategories/queue, POST /store/subcategories/:id/approve, POST /store/subcategories/:id/merge' },
|
|
241
|
+
// The project's authorised person for money (task 1004913, ADR 0385 D4): sign,
|
|
242
|
+
// narrow or revoke a standing payout rule, hold a noticed run, approve one payout,
|
|
243
|
+
// sign one equity grant or contract, and read the approval log. Archon by default;
|
|
244
|
+
// not a system key, so a project may hand it to a treasurer rank of its own. The
|
|
245
|
+
// routes additionally refuse anything but a person's own web session.
|
|
246
|
+
{ key: 'payout.approve', system: false, floor: 'archon', guards: 'GET /payouts/approvals, GET /payouts/rules, POST /payouts/rules[/:id/{narrow,revoke,runs,runs/:runKey/hold}], POST /payouts/{approve,grants,contracts}' },
|
|
241
247
|
// Take a module out of the store (task 1003797, ADR 0338 D2). Stops new installs,
|
|
242
248
|
// versions and downloads; never reaches an instance that already holds it.
|
|
243
249
|
{ key: 'module.delist', system: false, floor: 'metic', guards: 'POST /store/modules/:key/delist, POST /store/modules/:key/versions/:version/release (a version the promotion gate held on tester — task 1003804), POST /store/modules/:key/versions/:version/withdraw (take a version back — task 1004895)' },
|
package/modules/government/db.js
CHANGED
|
@@ -169,6 +169,79 @@ async function grantRankPermission({ rankKey, permissionKey }) {
|
|
|
169
169
|
return rowCount > 0;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// Add a recommended rank set in ONE transaction and three statements (task
|
|
173
|
+
// 1004929) — the batched form of "createRank, then grantRankPermission per key",
|
|
174
|
+
// which used to cost a read, an insert and one insert per grant for every rank,
|
|
175
|
+
// each its own round trip and none of them atomic. A rank that already exists is
|
|
176
|
+
// LEFT ALONE: it is neither inserted nor granted, and a rank a concurrent call
|
|
177
|
+
// created between the read and the insert is caught by ON CONFLICT DO NOTHING —
|
|
178
|
+
// the batched twin of the old per-row 23505 catch. Grants go only to the ranks
|
|
179
|
+
// THIS call created. Still a dumb write: `permissionKeys` arrive screened against
|
|
180
|
+
// the catalog by the route (routes/government.js), which owns that decision.
|
|
181
|
+
//
|
|
182
|
+
// `ranks`: [{ rankKey, label, description, permissionKeys }], in set order.
|
|
183
|
+
// Returns a Map of rank_key → the permission keys actually granted, in input
|
|
184
|
+
// order, for each rank this call created; a key absent from the Map was skipped.
|
|
185
|
+
async function applyRankSet({ ranks, createdBy }) {
|
|
186
|
+
const firstByKey = new Map();
|
|
187
|
+
for (const r of ranks) if (!firstByKey.has(r.rankKey)) firstByKey.set(r.rankKey, r);
|
|
188
|
+
const wanted = [...firstByKey.values()];
|
|
189
|
+
const created = new Map();
|
|
190
|
+
if (wanted.length === 0) return created;
|
|
191
|
+
const client = await pool.connect();
|
|
192
|
+
try {
|
|
193
|
+
await client.query('BEGIN');
|
|
194
|
+
const { rows: existing } = await client.query(
|
|
195
|
+
`SELECT rank_key FROM government_ranks WHERE rank_key = ANY($1::text[])`,
|
|
196
|
+
[wanted.map((r) => r.rankKey)],
|
|
197
|
+
);
|
|
198
|
+
const have = new Set(existing.map((row) => row.rank_key));
|
|
199
|
+
const fresh = wanted.filter((r) => !have.has(r.rankKey));
|
|
200
|
+
if (fresh.length > 0) {
|
|
201
|
+
// WITH ORDINALITY + ORDER BY keeps the ids in set order, as the loop did.
|
|
202
|
+
const { rows: inserted } = await client.query(
|
|
203
|
+
`INSERT INTO government_ranks (rank_key, label, description, is_system, seeded_rank, created_by)
|
|
204
|
+
SELECT r.rank_key, r.label, r.description, false, NULL, $4::bigint
|
|
205
|
+
FROM unnest($1::text[], $2::text[], $3::text[]) WITH ORDINALITY AS r(rank_key, label, description, ord)
|
|
206
|
+
ORDER BY r.ord
|
|
207
|
+
ON CONFLICT DO NOTHING
|
|
208
|
+
RETURNING rank_key`,
|
|
209
|
+
[fresh.map((r) => r.rankKey), fresh.map((r) => r.label), fresh.map((r) => r.description ?? null), createdBy],
|
|
210
|
+
);
|
|
211
|
+
const made = new Set(inserted.map((row) => row.rank_key));
|
|
212
|
+
const grantRanks = [];
|
|
213
|
+
const grantKeys = [];
|
|
214
|
+
for (const r of fresh) {
|
|
215
|
+
if (!made.has(r.rankKey)) continue;
|
|
216
|
+
created.set(r.rankKey, []);
|
|
217
|
+
for (const key of new Set(r.permissionKeys)) { grantRanks.push(r.rankKey); grantKeys.push(key); }
|
|
218
|
+
}
|
|
219
|
+
if (grantKeys.length > 0) {
|
|
220
|
+
const { rows: granted } = await client.query(
|
|
221
|
+
`INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
222
|
+
SELECT g.rank_key, g.permission_key
|
|
223
|
+
FROM unnest($1::text[], $2::text[]) WITH ORDINALITY AS g(rank_key, permission_key, ord)
|
|
224
|
+
ORDER BY g.ord
|
|
225
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING
|
|
226
|
+
RETURNING rank_key, permission_key`,
|
|
227
|
+
[grantRanks, grantKeys],
|
|
228
|
+
);
|
|
229
|
+
const landed = new Set(granted.map((row) => `${row.rank_key}\u0000${row.permission_key}`));
|
|
230
|
+
grantRanks.forEach((rk, i) => {
|
|
231
|
+
if (landed.has(`${rk}\u0000${grantKeys[i]}`)) created.get(rk).push(grantKeys[i]);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await client.query('COMMIT');
|
|
236
|
+
} catch (err) {
|
|
237
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
238
|
+
throw err;
|
|
239
|
+
} finally {
|
|
240
|
+
client.release();
|
|
241
|
+
}
|
|
242
|
+
return created;
|
|
243
|
+
}
|
|
244
|
+
|
|
172
245
|
async function revokeRankPermission({ rankKey, permissionKey }) {
|
|
173
246
|
const { rowCount } = await pool.query(
|
|
174
247
|
`DELETE FROM government_rank_permissions
|
|
@@ -356,9 +429,26 @@ async function openBoardItem({ subjectType, subjectId, openedBy = null, constitu
|
|
|
356
429
|
RETURNING *`,
|
|
357
430
|
[subjectType, subjectId, openedBy, windowMinutes, JSON.stringify(constitution || {})],
|
|
358
431
|
);
|
|
432
|
+
// Tell the expiry poller (task 1004960): it sleeps until the soonest deadline
|
|
433
|
+
// it knows of, so a window shorter than its idle wait must wake it sooner.
|
|
434
|
+
// closes_at is now() + the window, so the window IS the time until close.
|
|
435
|
+
// A listener the poller registers (not a require: the poller requires this
|
|
436
|
+
// file). Never let a hint fail an open.
|
|
437
|
+
if (rows[0] && windowMinutes != null) {
|
|
438
|
+
for (const fn of deadlineListeners) {
|
|
439
|
+
try { fn(Number(windowMinutes) * 60_000); } catch (_) { /* best-effort */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
359
442
|
return rows[0] || null;
|
|
360
443
|
}
|
|
361
444
|
|
|
445
|
+
// Who wants to hear that a timed window opened (the expiry poller's start()).
|
|
446
|
+
const deadlineListeners = new Set();
|
|
447
|
+
function onBoardDeadline(fn) {
|
|
448
|
+
deadlineListeners.add(fn);
|
|
449
|
+
return () => deadlineListeners.delete(fn);
|
|
450
|
+
}
|
|
451
|
+
|
|
362
452
|
async function getOpenBoardItem(subjectType, subjectId) {
|
|
363
453
|
const { rows } = await pool.query(
|
|
364
454
|
`SELECT * FROM government_board_items
|
|
@@ -1041,6 +1131,21 @@ async function getExpiredOpenBoardItems(limit = 100) {
|
|
|
1041
1131
|
return rows;
|
|
1042
1132
|
}
|
|
1043
1133
|
|
|
1134
|
+
// The expiry poller's alarm clock (task 1004960): how many milliseconds until
|
|
1135
|
+
// the soonest OPEN deadline, measured on the DATABASE clock (so a skewed
|
|
1136
|
+
// process clock can never make the poller wake early or late), or null when no
|
|
1137
|
+
// open item has a clock. ONE read, served by the same partial index as the
|
|
1138
|
+
// sweep — the poller sleeps until this deadline instead of asking every minute.
|
|
1139
|
+
async function msUntilSoonestOpenDeadline() {
|
|
1140
|
+
const { rows } = await pool.query(
|
|
1141
|
+
`SELECT EXTRACT(EPOCH FROM (MIN(closes_at) - now())) * 1000 AS ms
|
|
1142
|
+
FROM government_board_items
|
|
1143
|
+
WHERE closed_at IS NULL AND closes_at IS NOT NULL`,
|
|
1144
|
+
);
|
|
1145
|
+
const ms = rows[0] ? rows[0].ms : null;
|
|
1146
|
+
return ms == null ? null : Number(ms);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1044
1149
|
// The summons read (task 1003271): every OPEN item this builder could still
|
|
1045
1150
|
// vote on, with the two facts that decide it — who authored the subject, and
|
|
1046
1151
|
// whether this builder has already cast. Membership is NOT decided here: it
|
|
@@ -1165,6 +1270,7 @@ module.exports = {
|
|
|
1165
1270
|
getRankByKey,
|
|
1166
1271
|
createRank,
|
|
1167
1272
|
grantRankPermission,
|
|
1273
|
+
applyRankSet,
|
|
1168
1274
|
revokeRankPermission,
|
|
1169
1275
|
resetRankGrants,
|
|
1170
1276
|
deleteCustomRank,
|
|
@@ -1186,6 +1292,8 @@ module.exports = {
|
|
|
1186
1292
|
withdrawBoardItem,
|
|
1187
1293
|
getIdeaCompleteness,
|
|
1188
1294
|
getExpiredOpenBoardItems,
|
|
1295
|
+
msUntilSoonestOpenDeadline,
|
|
1296
|
+
onBoardDeadline,
|
|
1189
1297
|
getLatestReturnedItemWithObjection,
|
|
1190
1298
|
getLatestPassedItem,
|
|
1191
1299
|
listPassedSubjectIds,
|
|
@@ -85,6 +85,7 @@ const docketFloors = require('./docket-floors');
|
|
|
85
85
|
const governmentConfig = require('./config');
|
|
86
86
|
const asksDb = require('./asks-db');
|
|
87
87
|
const routing = require('./routing');
|
|
88
|
+
const waiting = require('./waiting');
|
|
88
89
|
const { ownerOf } = require('./leads');
|
|
89
90
|
|
|
90
91
|
const log = api.logger('government');
|
|
@@ -847,6 +848,40 @@ async function loadSeats(government) {
|
|
|
847
848
|
}
|
|
848
849
|
}
|
|
849
850
|
|
|
851
|
+
// OPEN BLOCKERS UNDER THE PEOPLE THEY WAIT ON (task 1004999). The blockers source is
|
|
852
|
+
// read exactly as the docket reads it (the ideas module's contribution, under its own
|
|
853
|
+
// `anyBuilder` gate — the list is public), each one carries its stored ask, and the
|
|
854
|
+
// seats are read once, uncached (ADR 0016). waiting.js groups them with routing.js's
|
|
855
|
+
// rule, so this answers "whose is it" exactly as the City does. A board seat's holders
|
|
856
|
+
// are read only when some blocker names one. → { available, people, nobody, yours }
|
|
857
|
+
// `available: false` names why nothing could be grouped — never an empty list
|
|
858
|
+
// standing in for "nobody is waiting".
|
|
859
|
+
async function blockersByPerson(builder, { contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
|
|
860
|
+
const c = contributions.sources.get('blockers');
|
|
861
|
+
if (!c) return { available: false, reason: 'off', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
862
|
+
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
863
|
+
const held = new Set(permissions || []);
|
|
864
|
+
const ctx = Object.freeze({ builderId: String(builder.id), rank: builder.rank || null, has: (p) => held.has(p) });
|
|
865
|
+
if (!gateAllows(c.gate, ctx)) return { available: false, reason: 'not_yours', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
866
|
+
let matters;
|
|
867
|
+
try {
|
|
868
|
+
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
|
|
869
|
+
matters = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source: 'blockers', building: c.building || 'planning' })).filter(Boolean);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
log.warn({ err: err && err.message }, 'blockers by person: blockers unreadable');
|
|
872
|
+
return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
873
|
+
}
|
|
874
|
+
await attachAsks(matters, asksDb.asksFor, timeoutMs);
|
|
875
|
+
const boardSeat = matters.some((m) => /^board:/.test(routing.seatOf(m)));
|
|
876
|
+
const government = boardSeat
|
|
877
|
+
? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
|
|
878
|
+
: { holders: {} };
|
|
879
|
+
const seats = await loadSeats(government);
|
|
880
|
+
if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
881
|
+
const grouped = waiting.groupByPerson(matters, seats);
|
|
882
|
+
return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
|
|
883
|
+
}
|
|
884
|
+
|
|
850
885
|
// One matter with its stored ask and the seats, for passing it on to `toSeat`
|
|
851
886
|
// (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
|
|
852
887
|
// board's holders are read only when a board seat is involved.
|
|
@@ -927,5 +962,6 @@ module.exports = {
|
|
|
927
962
|
findMatter,
|
|
928
963
|
findMatterFor,
|
|
929
964
|
loadSeats,
|
|
965
|
+
blockersByPerson,
|
|
930
966
|
matterWithSeats,
|
|
931
967
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- government_035_payout_approve.sql — grant the new `payout.approve` atom to
|
|
2
|
+
-- Archon (task 1004913, goal 1000092 N9; ADR 0385 D4).
|
|
3
|
+
--
|
|
4
|
+
-- WHAT IT IS. The project's authorised person for money: signs a standing payout
|
|
5
|
+
-- rule, narrows or revokes it, holds a run in its pause window, approves a single
|
|
6
|
+
-- payout, signs an equity grant or a contract, and reads the approval log
|
|
7
|
+
-- (core_283_payout_approvals). The routes also demand the person's own web
|
|
8
|
+
-- session, so holding the atom on a CLI token is not enough to sign.
|
|
9
|
+
--
|
|
10
|
+
-- WHY THIS FILE EXISTS. RANK_SEED is derived from the catalog's floors, and the
|
|
11
|
+
-- drift guard (tests/government_seed.mjs) compares it against the union of the
|
|
12
|
+
-- run-once seed plus every later grant migration, per rank. An archon-floor key
|
|
13
|
+
-- is held by archon only.
|
|
14
|
+
--
|
|
15
|
+
-- Additive only: one new permission key granted to one rank; nothing is revoked.
|
|
16
|
+
-- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
|
|
17
|
+
--
|
|
18
|
+
-- The `SELECT '<rank>', unnest(ARRAY[…])` shape is load-bearing: the drift guard
|
|
19
|
+
-- scans for it literally.
|
|
20
|
+
|
|
21
|
+
BEGIN;
|
|
22
|
+
|
|
23
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
24
|
+
SELECT 'archon', unnest(ARRAY[
|
|
25
|
+
'payout.approve'
|
|
26
|
+
])
|
|
27
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
28
|
+
|
|
29
|
+
COMMIT;
|
|
@@ -19,17 +19,32 @@
|
|
|
19
19
|
// minute-cadence poller logging every tick would bury the journal (the
|
|
20
20
|
// reconciler's own warn-once lesson).
|
|
21
21
|
//
|
|
22
|
-
// CADENCE:
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
22
|
+
// CADENCE (task 1004960): the poller SLEEPS UNTIL THE SOONEST DEADLINE instead
|
|
23
|
+
// of asking every minute. After each sweep it makes one MIN(closes_at) read
|
|
24
|
+
// (msUntilSoonestOpenDeadline, the same partial index) and arms its next run
|
|
25
|
+
// for just after that deadline — capped at MAX_WAIT_MS (5 minutes) so an item
|
|
26
|
+
// opened by some other process is still caught. A window opened in THIS process
|
|
27
|
+
// wakes it sooner through noteDeadline (db.openBoardItem calls it), so a
|
|
28
|
+
// one-minute window still closes on time. An idle instance with no open clock
|
|
29
|
+
// therefore reads twice per 5 minutes, not once a minute; a vote still closes
|
|
30
|
+
// within ~1 second of its deadline. While something is OVERDUE after a sweep
|
|
31
|
+
// (its close threw and rolled back) the retry waits RETRY_MS (the old one-minute
|
|
32
|
+
// cadence), never a hot loop. Runs never overlap: a tick that fires while the
|
|
33
|
+
// previous sweep is still in flight is skipped. Concurrent windows are normal
|
|
27
34
|
// (owner decision 10): the sweep closes every due item, oldest deadline first.
|
|
28
35
|
|
|
29
36
|
const board = require('../board');
|
|
30
37
|
const db = require('../db');
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
// The longest the poller ever sleeps (BOARD_EXPIRY_SWEEP_MS overrides it). It
|
|
40
|
+
// only bounds an item opened by ANOTHER process; this process's opens wake it.
|
|
41
|
+
const MAX_WAIT_MS = Number(process.env.BOARD_EXPIRY_SWEEP_MS) || 5 * 60_000;
|
|
42
|
+
// Kept as an alias: INTERVAL_MS was the fixed cadence before task 1004960.
|
|
43
|
+
const INTERVAL_MS = MAX_WAIT_MS;
|
|
44
|
+
// While an item is overdue after a sweep (its close failed and rolled back).
|
|
45
|
+
const RETRY_MS = 60_000;
|
|
46
|
+
// Wake just AFTER a deadline, since the sweep reads closes_at < now().
|
|
47
|
+
const GRACE_MS = 1_000;
|
|
33
48
|
// A first sweep shortly after boot, so windows that expired while the process
|
|
34
49
|
// was down (a deploy restart mid-sitting) close promptly rather than one full
|
|
35
50
|
// interval late.
|
|
@@ -38,8 +53,21 @@ const FIRST_SWEEP_DELAY_MS = Number(process.env.BOARD_EXPIRY_FIRST_MS) || 15_000
|
|
|
38
53
|
// handful of open windows (the board's subjects are rare by construction).
|
|
39
54
|
const SWEEP_LIMIT = 100;
|
|
40
55
|
|
|
41
|
-
let
|
|
42
|
-
let
|
|
56
|
+
let timer = null;
|
|
57
|
+
let dueAt = null; // when the armed timer fires (process clock, ms)
|
|
58
|
+
let started = false;
|
|
59
|
+
let running = false;
|
|
60
|
+
let hintAt = null; // soonest deadline (process clock) noted mid-sweep
|
|
61
|
+
let startDeps = {};
|
|
62
|
+
let unlisten = null;
|
|
63
|
+
|
|
64
|
+
// When to run next, given the ms until the soonest open deadline (null: none).
|
|
65
|
+
function nextDelayMs(msUntilSoonest) {
|
|
66
|
+
if (msUntilSoonest == null || !Number.isFinite(Number(msUntilSoonest))) return MAX_WAIT_MS;
|
|
67
|
+
const ms = Number(msUntilSoonest);
|
|
68
|
+
if (ms <= 0) return RETRY_MS; // still overdue after a sweep: retry, never spin
|
|
69
|
+
return Math.min(MAX_WAIT_MS, ms + GRACE_MS);
|
|
70
|
+
}
|
|
43
71
|
|
|
44
72
|
// One sweep. Injectable deps keep it DB-free testable: listFn yields the due
|
|
45
73
|
// items, closeFn closes one, log receives the disposition lines.
|
|
@@ -82,20 +110,80 @@ function start(deps = {}) {
|
|
|
82
110
|
console.log('[board-expiry] disabled (BOARD_EXPIRY_DISABLED=1)');
|
|
83
111
|
return;
|
|
84
112
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
if (started) return; // already started
|
|
114
|
+
started = true;
|
|
115
|
+
startDeps = deps;
|
|
116
|
+
unlisten = db.onBoardDeadline(noteDeadline); // a window opened here wakes us sooner
|
|
117
|
+
arm(FIRST_SWEEP_DELAY_MS);
|
|
118
|
+
console.log(`[board-expiry] started (sleeps until the soonest deadline, at most ${Math.round(MAX_WAIT_MS / 1000)}s; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function arm(delay) {
|
|
122
|
+
if (timer) clearTimeout(timer);
|
|
123
|
+
dueAt = Date.now() + delay;
|
|
124
|
+
timer = setTimeout(() => {
|
|
125
|
+
timer = null;
|
|
126
|
+
dueAt = null;
|
|
127
|
+
tick().catch((e) => console.error(`[board-expiry] sweep crashed: ${e && e.message}`));
|
|
128
|
+
}, delay);
|
|
129
|
+
if (timer.unref) timer.unref(); // never hold the process open on a restart
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One run: the sweep, then the one MIN(closes_at) read that sets the alarm.
|
|
133
|
+
// A run that starts while the previous one is still in flight is skipped —
|
|
134
|
+
// the running one re-arms when it finishes, so nothing is lost.
|
|
135
|
+
async function tick(deps = startDeps) {
|
|
136
|
+
if (running) return { skipped: true };
|
|
137
|
+
running = true;
|
|
138
|
+
let soonest = null;
|
|
139
|
+
try {
|
|
140
|
+
const out = await runSweep(deps);
|
|
141
|
+
try {
|
|
142
|
+
soonest = await (deps.soonestFn || db.msUntilSoonestOpenDeadline)();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
(deps.error || console.error)(`[board-expiry] soonest-deadline read failed: ${e && e.message}`);
|
|
145
|
+
soonest = 0; // unknown: fall back to the one-minute retry, not the 5-minute cap
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
} finally {
|
|
149
|
+
running = false;
|
|
150
|
+
if (hintAt != null) {
|
|
151
|
+
const hint = hintAt - Date.now();
|
|
152
|
+
soonest = soonest == null ? hint : Math.min(soonest, hint);
|
|
153
|
+
hintAt = null;
|
|
154
|
+
}
|
|
155
|
+
if (started) arm(nextDelayMs(soonest));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A window was opened that closes msUntilClose from now: wake sooner if the
|
|
160
|
+
// armed alarm is later than that deadline. Mid-sweep, remember it for the re-arm.
|
|
161
|
+
function noteDeadline(msUntilClose) {
|
|
162
|
+
if (!started || msUntilClose == null || !Number.isFinite(Number(msUntilClose))) return;
|
|
163
|
+
const ms = Math.max(0, Number(msUntilClose));
|
|
164
|
+
if (running) {
|
|
165
|
+
const at = Date.now() + ms;
|
|
166
|
+
hintAt = hintAt == null ? at : Math.min(hintAt, at);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const delay = Math.min(MAX_WAIT_MS, ms + GRACE_MS);
|
|
170
|
+
if (dueAt == null || Date.now() + delay < dueAt) arm(delay);
|
|
94
171
|
}
|
|
95
172
|
|
|
96
173
|
function stop() {
|
|
97
|
-
|
|
98
|
-
|
|
174
|
+
started = false;
|
|
175
|
+
hintAt = null;
|
|
176
|
+
if (unlisten) { unlisten(); unlisten = null; }
|
|
177
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
178
|
+
dueAt = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// For tests: what is armed right now.
|
|
182
|
+
function _state() {
|
|
183
|
+
return { started, running, msUntilNextRun: dueAt == null ? null : dueAt - Date.now() };
|
|
99
184
|
}
|
|
100
185
|
|
|
101
|
-
module.exports = {
|
|
186
|
+
module.exports = {
|
|
187
|
+
start, stop, runSweep, tick, noteDeadline, nextDelayMs, _state,
|
|
188
|
+
INTERVAL_MS, MAX_WAIT_MS, RETRY_MS, GRACE_MS, SWEEP_LIMIT,
|
|
189
|
+
};
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
{ "glob": "src/bongos/auth-config.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
|
|
31
31
|
{ "glob": "src/bongos/auth-discord.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
|
|
32
32
|
{ "glob": "src/bongos/auth-github.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
|
|
33
|
+
{ "glob": "src/bongos/auth-google.js", "group": "authz", "why": "task 1004993 (ADR 0396). The Google sign-in beside auth-github.js: it checks Google's ID token, runs the admission gate for a Google person and mints their web session, so it is the same trust surface as the GitHub flow." },
|
|
33
34
|
{ "glob": "src/bongos/auth-idp.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
|
|
34
35
|
{ "glob": "src/bongos/db.js", "group": "authz", "why": "rank tiers, REQUIRES_RANK_TIER — the CASE edited in the SEC #863 incident" },
|
|
35
36
|
{ "glob": "src/bongos/db-kernel.js", "group": "authz", "why": "task 1004024. The kernel db slice carved out of db.js (BV1.R69 / ADR 0091): builder identity and the rank reads the SEC #863 entry above exists for." },
|
|
@@ -434,40 +434,50 @@ module.exports = function buildGovernmentRouter() {
|
|
|
434
434
|
|
|
435
435
|
// rank: perm government.manage — add a RECOMMENDED RANK SET (task 1004400):
|
|
436
436
|
// the custom ranks recommendations.js names for a project type and size, each
|
|
437
|
-
// with its grants. It
|
|
438
|
-
// under the same checks: a rank that already exists is LEFT ALONE (never
|
|
437
|
+
// with its grants. It does what the create form and the grant toggle do, for
|
|
438
|
+
// every rank in the set, under the same checks: a rank that already exists is LEFT ALONE (never
|
|
439
439
|
// re-granted, never relabelled — an owner may have edited it), and every grant
|
|
440
440
|
// is re-checked with catalog.isGrantableTo, so a set can never write what the
|
|
441
441
|
// toggle would refuse. Re-applying is safe; the reply says what was added.
|
|
442
|
+
//
|
|
443
|
+
// task 1004929: the whole set is written by db.applyRankSet in ONE transaction
|
|
444
|
+
// and three statements, instead of a read + insert + one insert per grant for
|
|
445
|
+
// every rank. A rank this route creates is always a CUSTOM rank (is_system
|
|
446
|
+
// false, no seeded_rank — what createRank returns), so each grant is screened
|
|
447
|
+
// against that shape before the write, exactly as the toggle would screen it.
|
|
448
|
+
// A failure now writes nothing, so a 500 reports empty created/skipped lists.
|
|
442
449
|
router.post('/government/rank-sets/apply', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
|
|
443
450
|
if (validateOrRespond(req, res, {
|
|
444
451
|
type: { type: 'string', maxLength: 32 },
|
|
445
452
|
headcount: { required: true, type: 'integer', min: 0, max: 100000 },
|
|
446
453
|
})) return;
|
|
447
454
|
const set = recommendations.recommendedRankSet(req.body.type, req.body.headcount);
|
|
448
|
-
const
|
|
449
|
-
const skipped = [];
|
|
455
|
+
const customRank = { is_system: false, seeded_rank: null };
|
|
450
456
|
try {
|
|
457
|
+
const granted = await db.applyRankSet({
|
|
458
|
+
createdBy: req.builder.id,
|
|
459
|
+
ranks: set.ranks.map((r) => ({
|
|
460
|
+
rankKey: r.rank_key,
|
|
461
|
+
label: r.label,
|
|
462
|
+
description: r.description,
|
|
463
|
+
permissionKeys: r.permissions.filter((key) => catalog.byKey(key) && catalog.isGrantableTo(key, customRank)),
|
|
464
|
+
})),
|
|
465
|
+
});
|
|
466
|
+
const created = [];
|
|
467
|
+
const skipped = [];
|
|
451
468
|
for (const r of set.ranks) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
throw err;
|
|
469
|
+
// delete-on-read: a key named twice in a set is created once, then skipped.
|
|
470
|
+
if (granted.has(r.rank_key)) {
|
|
471
|
+
created.push({ rank_key: r.rank_key, label: r.label, permissions: granted.get(r.rank_key) });
|
|
472
|
+
granted.delete(r.rank_key);
|
|
473
|
+
} else {
|
|
474
|
+
skipped.push({ rank_key: r.rank_key, label: r.label });
|
|
459
475
|
}
|
|
460
|
-
const granted = [];
|
|
461
|
-
for (const key of r.permissions) {
|
|
462
|
-
if (!catalog.byKey(key) || !catalog.isGrantableTo(key, rank)) continue;
|
|
463
|
-
if (await db.grantRankPermission({ rankKey: r.rank_key, permissionKey: key })) granted.push(key);
|
|
464
|
-
}
|
|
465
|
-
created.push({ rank_key: r.rank_key, label: r.label, permissions: granted });
|
|
466
476
|
}
|
|
467
477
|
res.json({ ok: true, type: set.type, size: set.size, created, skipped });
|
|
468
478
|
} catch (err) {
|
|
469
479
|
log.error('[gds] POST /government/rank-sets/apply', err);
|
|
470
|
-
res.fail('rank_set_apply_failed', { status: 500, message: 'internal error', details: { created, skipped } });
|
|
480
|
+
res.fail('rank_set_apply_failed', { status: 500, message: 'internal error', details: { created: [], skipped: [] } });
|
|
471
481
|
}
|
|
472
482
|
});
|
|
473
483
|
|
|
@@ -757,6 +767,28 @@ module.exports = function buildGovernmentRouter() {
|
|
|
757
767
|
// treasury; `rule` ∈ decide first consent majority unanimous signoff rule (or
|
|
758
768
|
// null); `state` ∈ needs moving settled. Nothing is ever sample data: a source
|
|
759
769
|
// with nothing recorded yet is `coming`, never a placeholder row.
|
|
770
|
+
// OPEN BLOCKERS UNDER PEOPLE (task 1004999): every open blocker grouped under each
|
|
771
|
+
// person its seat resolves to (routing.js, the City's own rule), and the viewer's own
|
|
772
|
+
// group. The docket's atom: it floors at xenos, so every builder can see whose a
|
|
773
|
+
// blocker is; resolving one keeps its own blocker.review gate.
|
|
774
|
+
router.get('/government/blockers/people', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
775
|
+
try {
|
|
776
|
+
res.json(await docket.blockersByPerson(req.builder));
|
|
777
|
+
} catch (err) {
|
|
778
|
+
log.error('[gds] GET /government/blockers/people', err);
|
|
779
|
+
res.fail('blockers_people_read_failed', { status: 500, message: 'internal error' });
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
router.get('/government/blockers/for-me', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
783
|
+
try {
|
|
784
|
+
const r = await docket.blockersByPerson(req.builder);
|
|
785
|
+
res.json({ available: r.available, reason: r.reason || null, ...r.yours });
|
|
786
|
+
} catch (err) {
|
|
787
|
+
log.error('[gds] GET /government/blockers/for-me', err);
|
|
788
|
+
res.fail('blockers_for_me_read_failed', { status: 500, message: 'internal error' });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
760
792
|
router.get('/government/docket', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
761
793
|
try {
|
|
762
794
|
res.json(await docket.docketFor(req.builder));
|