@bongos/core 1.23.22 → 1.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +933 -413
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +57 -7
- package/docs/page-readings.json +1210 -1027
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +158 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +38 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +230 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/government-walk.js +3 -0
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/access_holders.mjs +214 -0
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/access-routes.js — the handlers behind the three ACCESS HOLDERS
|
|
4
|
+
// routes (task 1005000). The routes themselves — path, permission gate and pin — are
|
|
5
|
+
// declared in routes/government.js, where route-rank-check reads them; only the bodies
|
|
6
|
+
// live here, so the router file stays under the size ratchet.
|
|
7
|
+
//
|
|
8
|
+
// GET /government/access (board.vote.cast) — read
|
|
9
|
+
// POST /government/access/:accessKey/holders (government.manage) — record
|
|
10
|
+
// DELETE /government/access/:accessKey/holders/:builderId (government.manage) — remove
|
|
11
|
+
//
|
|
12
|
+
// ⚠ A record of who to ask, never authority (access.js). Not act-alone routes: they
|
|
13
|
+
// record a fact about the world outside the project; they decide nothing a sitting
|
|
14
|
+
// votes on.
|
|
15
|
+
|
|
16
|
+
const access = require('./access');
|
|
17
|
+
|
|
18
|
+
// deps: { db, log, parseId, validateOrRespond } — the router's own, passed in so a
|
|
19
|
+
// test that stubs the db by property reaches these handlers too.
|
|
20
|
+
function accessHandlers({ db, log, parseId, validateOrRespond }) {
|
|
21
|
+
const keyOf = (req, res) => {
|
|
22
|
+
const key = String(req.params.accessKey || '');
|
|
23
|
+
if (access.isAccessKey(key)) return key;
|
|
24
|
+
res.fail('unknown_access', { status: 400, message: `Name one of: ${access.KEYS.join(', ')}.` });
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function read(req, res) {
|
|
29
|
+
try {
|
|
30
|
+
res.json({ access: access.accessView(await db.listAccessHolders()) });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.error('[gds] GET /government/access', err);
|
|
33
|
+
res.fail('access_read_failed', { status: 500, message: 'internal error' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function grant(req, res) {
|
|
38
|
+
if (validateOrRespond(req, res, { builder_id: { required: true, type: 'integer', min: 1 } })) return;
|
|
39
|
+
const key = keyOf(req, res);
|
|
40
|
+
if (key === null) return;
|
|
41
|
+
const builderId = req.body.builder_id;
|
|
42
|
+
try {
|
|
43
|
+
const who = await db.getActiveBuilder(builderId);
|
|
44
|
+
if (!who) return res.fail('builder_not_found', { status: 404, message: 'No active builder with that id.' });
|
|
45
|
+
const added = await db.grantAccess(key, builderId, req.builder.id);
|
|
46
|
+
res.status(added ? 201 : 200).json({ ok: true, access_key: key, builder_id: String(builderId), added });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log.error('[gds] POST /government/access/:accessKey/holders', err);
|
|
49
|
+
res.fail('access_grant_failed', { status: 500, message: 'internal error' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function revoke(req, res) {
|
|
54
|
+
const key = keyOf(req, res);
|
|
55
|
+
if (key === null) return;
|
|
56
|
+
const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
|
|
57
|
+
if (builderId === null) return;
|
|
58
|
+
try {
|
|
59
|
+
const removed = await db.revokeAccess(key, builderId);
|
|
60
|
+
res.json({ ok: true, access_key: key, builder_id: String(builderId), removed });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.error('[gds] DELETE /government/access/:accessKey/holders/:builderId', err);
|
|
63
|
+
res.fail('access_revoke_failed', { status: 500, message: 'internal error' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { read, grant, revoke };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { accessHandlers };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/access.js — ACCESS HOLDERS: who holds the keys that live OUTSIDE
|
|
4
|
+
// Bongos, so a blocker that needs one reaches the person who has it (task 1005000,
|
|
5
|
+
// goal 1000111).
|
|
6
|
+
//
|
|
7
|
+
// WHY. Some blockers can only be cleared by whoever holds access no rank or Lead
|
|
8
|
+
// models: the GitHub repository's admin (token scopes, branch protection, the App's
|
|
9
|
+
// permissions), the server, the domain's DNS, the hosting account, package publishing,
|
|
10
|
+
// payments. An Archon in Bongos may not hold the GitHub admin, and a Lead never does.
|
|
11
|
+
// It lived only in prose (docs/credentials.md "account logins the owner holds") and in
|
|
12
|
+
// blocker text — blocker 1000199, the publish token missing GitHub's `workflow` scope,
|
|
13
|
+
// could only be filed to "the owner". The builder's words: repo owner is a category of
|
|
14
|
+
// blockers that go beyond normality.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IT IS. A fixed vocabulary of outside-access keys (ACCESS, extendable here and in
|
|
17
|
+
// the migration's CHECK together), a record of which builders hold each
|
|
18
|
+
// (government_access_holders, written by the owner only), and a seat, `access:<key>`,
|
|
19
|
+
// that an ask can wait on. routing.js resolves that seat to the recorded holders, and
|
|
20
|
+
// to the owner when nobody is recorded — the same fall every other seat takes.
|
|
21
|
+
//
|
|
22
|
+
// ⚠ A RECORD OF WHO TO ASK, NEVER AN AUTHORITY GRANT. Holding github-admin here unlocks
|
|
23
|
+
// no Bongos route and no rank: this module is not read by requireRank, the permission
|
|
24
|
+
// resolver, or any gate, and an access key is not a permission atom. No secret is
|
|
25
|
+
// stored either — the row says WHO holds an access, never a credential or a token.
|
|
26
|
+
//
|
|
27
|
+
// PURE: no db.
|
|
28
|
+
|
|
29
|
+
const ACCESS = Object.freeze([
|
|
30
|
+
// name: how the seat reads after "waits on the"; needs: what a blocker on it needs.
|
|
31
|
+
Object.freeze({ key: 'github-admin', name: 'repo owner', needs: 'repo-owner access', what: 'Admin on the GitHub repository: token scopes, branch protection, the GitHub App.' }),
|
|
32
|
+
Object.freeze({ key: 'server-ssh', name: 'server access', needs: 'server access', what: 'A login on the server the project runs on.' }),
|
|
33
|
+
Object.freeze({ key: 'dns', name: 'domain settings', needs: 'access to the domain settings', what: 'The DNS account for the project\'s domain.' }),
|
|
34
|
+
Object.freeze({ key: 'hosting', name: 'hosting account', needs: 'access to the hosting account', what: 'The account that pays for and runs the project\'s servers.' }),
|
|
35
|
+
Object.freeze({ key: 'npm-publish', name: 'package publishing', needs: 'package publishing rights', what: 'Publishing rights for the project\'s packages.' }),
|
|
36
|
+
Object.freeze({ key: 'payments', name: 'payments', needs: 'access to payments', what: 'The account money moves through.' }),
|
|
37
|
+
]);
|
|
38
|
+
const KEYS = Object.freeze(ACCESS.map((a) => a.key));
|
|
39
|
+
const isAccessKey = (k) => KEYS.includes(String(k || ''));
|
|
40
|
+
const byKey = (k) => ACCESS.find((a) => a.key === k) || null;
|
|
41
|
+
|
|
42
|
+
// The seat grammar's access half, for asks.js SEAT and the migration's CHECK.
|
|
43
|
+
const SEAT_PART = `access:(${KEYS.join('|')})`;
|
|
44
|
+
|
|
45
|
+
// "the repo owner" — how the seat reads after "waits on".
|
|
46
|
+
function accessLabel(key) {
|
|
47
|
+
const a = byKey(key);
|
|
48
|
+
return a ? `the ${a.name}` : 'the owner';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// "needs repo-owner access" — how a blocker on this seat reads under its holder.
|
|
52
|
+
function needsWords(key) {
|
|
53
|
+
const a = byKey(key);
|
|
54
|
+
return a ? `needs ${a.needs}` : 'needs outside access';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The holders read as the seat resolver wants them: { key: [{ builder_id, github_login, display_name }] }.
|
|
58
|
+
function holdersByKey(rows) {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const k of KEYS) out[k] = [];
|
|
61
|
+
for (const r of rows || []) {
|
|
62
|
+
if (r && isAccessKey(r.access_key) && r.builder_id != null) out[r.access_key].push(r);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The read GET /government/access answers: every access, its words, who holds it.
|
|
68
|
+
function accessView(rows) {
|
|
69
|
+
const held = holdersByKey(rows);
|
|
70
|
+
return ACCESS.map((a) => ({
|
|
71
|
+
key: a.key,
|
|
72
|
+
name: a.name,
|
|
73
|
+
// the one display form, for the list and the picker alike
|
|
74
|
+
title: a.name.charAt(0).toUpperCase() + a.name.slice(1),
|
|
75
|
+
what: a.what,
|
|
76
|
+
seat: `access:${a.key}`,
|
|
77
|
+
holders: held[a.key].map((r) => ({
|
|
78
|
+
id: String(r.builder_id),
|
|
79
|
+
login: r.github_login || null,
|
|
80
|
+
name: r.display_name || r.github_login || null,
|
|
81
|
+
since: r.granted_at || null,
|
|
82
|
+
})),
|
|
83
|
+
// nobody recorded: the seat falls to the owner, as every unheld seat does
|
|
84
|
+
falls_to_owner: held[a.key].length === 0,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { ACCESS, KEYS, SEAT_PART, isAccessKey, accessLabel, needsWords, holdersByKey, accessView };
|
|
@@ -44,10 +44,12 @@ const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
|
|
|
44
44
|
// Control characters other than tab and line breaks (line breaks have their own rule).
|
|
45
45
|
// eslint-disable-next-line no-control-regex
|
|
46
46
|
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
47
|
+
const access = require('./access');
|
|
47
48
|
const BUILDINGS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
|
|
48
49
|
// The seats an ask can wait on (gov-living.js seatResolver's grammar). An
|
|
49
50
|
// unseated building falls to the owner when it is routed (owner ruling).
|
|
50
|
-
|
|
51
|
+
// `access:<key>` (task 1005000): whoever holds that outside access (access.js).
|
|
52
|
+
const SEAT = new RegExp(`^(owner|(lead|board):(${BUILDINGS.join('|')})|committee:(${BUILDINGS.join('|')})\\.[a-z][a-z0-9_]{0,31}|${access.SEAT_PART})$`);
|
|
51
53
|
// An answer's key: what a tap sends. Its act, optional: the matter's own act it
|
|
52
54
|
// takes ('go ahead', 'record', 'force:tested_by_hand'); the deck maps it onto
|
|
53
55
|
// the desk row's real route and refuses one the row does not offer.
|
|
@@ -150,7 +152,7 @@ function checkAsk(raw) {
|
|
|
150
152
|
// ---- who it waits on: a seat the direction desk can route ----
|
|
151
153
|
const seat = str(c.waits_on).trim();
|
|
152
154
|
if (!seat) err('waits_on', 'missing', 'Say who this waits on.');
|
|
153
|
-
else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name> or
|
|
155
|
+
else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.`);
|
|
154
156
|
|
|
155
157
|
// ---- what happens if nobody answers ----
|
|
156
158
|
lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
|
|
@@ -206,6 +208,7 @@ const ASK_REQUIRED_MESSAGE = 'An AI session files a blocker with an ask: a quest
|
|
|
206
208
|
function seatWords(seat) {
|
|
207
209
|
const s = str(seat);
|
|
208
210
|
if (s === 'owner') return 'the owner';
|
|
211
|
+
if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
|
|
209
212
|
const m = /^(lead|board|committee):([a-z]+)(?:\.([a-z0-9_]+))?$/.exec(s);
|
|
210
213
|
if (!m) return s;
|
|
211
214
|
const building = m[2].charAt(0).toUpperCase() + m[2].slice(1);
|
|
@@ -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
|
@@ -429,9 +429,26 @@ async function openBoardItem({ subjectType, subjectId, openedBy = null, constitu
|
|
|
429
429
|
RETURNING *`,
|
|
430
430
|
[subjectType, subjectId, openedBy, windowMinutes, JSON.stringify(constitution || {})],
|
|
431
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
|
+
}
|
|
432
442
|
return rows[0] || null;
|
|
433
443
|
}
|
|
434
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
|
+
|
|
435
452
|
async function getOpenBoardItem(subjectType, subjectId) {
|
|
436
453
|
const { rows } = await pool.query(
|
|
437
454
|
`SELECT * FROM government_board_items
|
|
@@ -871,6 +888,41 @@ async function listActiveArchons() {
|
|
|
871
888
|
return rows;
|
|
872
889
|
}
|
|
873
890
|
|
|
891
|
+
// ACCESS HOLDERS (task 1005000, government_036): who holds each outside access.
|
|
892
|
+
// Active builders only — someone who left the project is nobody to ask. A record
|
|
893
|
+
// of who to ask, never authority: no gate reads these rows (access.js).
|
|
894
|
+
async function listAccessHolders() {
|
|
895
|
+
const { rows } = await pool.query(
|
|
896
|
+
`SELECT h.access_key, h.builder_id, h.granted_at, b.github_login, b.display_name
|
|
897
|
+
FROM government_access_holders h
|
|
898
|
+
JOIN builders b ON b.id = h.builder_id
|
|
899
|
+
WHERE b.status = 'active' AND b.system_role IS NULL
|
|
900
|
+
ORDER BY h.access_key, h.granted_at, h.builder_id`,
|
|
901
|
+
);
|
|
902
|
+
return rows;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Record that a builder holds an access. Idempotent: an existing row is kept as it
|
|
906
|
+
// was (its original granted_at/by). → true when a row was added.
|
|
907
|
+
async function grantAccess(accessKey, builderId, grantedBy) {
|
|
908
|
+
const { rowCount } = await pool.query(
|
|
909
|
+
`INSERT INTO government_access_holders (access_key, builder_id, granted_by)
|
|
910
|
+
VALUES ($1, $2, $3)
|
|
911
|
+
ON CONFLICT (access_key, builder_id) DO NOTHING`,
|
|
912
|
+
[accessKey, builderId, grantedBy],
|
|
913
|
+
);
|
|
914
|
+
return rowCount > 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// → true when a row was removed.
|
|
918
|
+
async function revokeAccess(accessKey, builderId) {
|
|
919
|
+
const { rowCount } = await pool.query(
|
|
920
|
+
'DELETE FROM government_access_holders WHERE access_key = $1 AND builder_id = $2',
|
|
921
|
+
[accessKey, builderId],
|
|
922
|
+
);
|
|
923
|
+
return rowCount > 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
874
926
|
// One builder of this project, or null: who a Lead appointment names. Only an
|
|
875
927
|
// active person (no machine identity) can be appointed.
|
|
876
928
|
async function getActiveBuilder(builderId) {
|
|
@@ -1114,6 +1166,21 @@ async function getExpiredOpenBoardItems(limit = 100) {
|
|
|
1114
1166
|
return rows;
|
|
1115
1167
|
}
|
|
1116
1168
|
|
|
1169
|
+
// The expiry poller's alarm clock (task 1004960): how many milliseconds until
|
|
1170
|
+
// the soonest OPEN deadline, measured on the DATABASE clock (so a skewed
|
|
1171
|
+
// process clock can never make the poller wake early or late), or null when no
|
|
1172
|
+
// open item has a clock. ONE read, served by the same partial index as the
|
|
1173
|
+
// sweep — the poller sleeps until this deadline instead of asking every minute.
|
|
1174
|
+
async function msUntilSoonestOpenDeadline() {
|
|
1175
|
+
const { rows } = await pool.query(
|
|
1176
|
+
`SELECT EXTRACT(EPOCH FROM (MIN(closes_at) - now())) * 1000 AS ms
|
|
1177
|
+
FROM government_board_items
|
|
1178
|
+
WHERE closed_at IS NULL AND closes_at IS NOT NULL`,
|
|
1179
|
+
);
|
|
1180
|
+
const ms = rows[0] ? rows[0].ms : null;
|
|
1181
|
+
return ms == null ? null : Number(ms);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1117
1184
|
// The summons read (task 1003271): every OPEN item this builder could still
|
|
1118
1185
|
// vote on, with the two facts that decide it — who authored the subject, and
|
|
1119
1186
|
// whether this builder has already cast. Membership is NOT decided here: it
|
|
@@ -1260,6 +1327,8 @@ module.exports = {
|
|
|
1260
1327
|
withdrawBoardItem,
|
|
1261
1328
|
getIdeaCompleteness,
|
|
1262
1329
|
getExpiredOpenBoardItems,
|
|
1330
|
+
msUntilSoonestOpenDeadline,
|
|
1331
|
+
onBoardDeadline,
|
|
1263
1332
|
getLatestReturnedItemWithObjection,
|
|
1264
1333
|
getLatestPassedItem,
|
|
1265
1334
|
listPassedSubjectIds,
|
|
@@ -1272,6 +1341,9 @@ module.exports = {
|
|
|
1272
1341
|
applyLaws,
|
|
1273
1342
|
listLeads,
|
|
1274
1343
|
listActiveArchons,
|
|
1344
|
+
listAccessHolders,
|
|
1345
|
+
grantAccess,
|
|
1346
|
+
revokeAccess,
|
|
1275
1347
|
getActiveBuilder,
|
|
1276
1348
|
applyLeadAct,
|
|
1277
1349
|
listBuildouts,
|
|
@@ -85,6 +85,8 @@ 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');
|
|
89
|
+
const access = require('./access');
|
|
88
90
|
const { ownerOf } = require('./leads');
|
|
89
91
|
|
|
90
92
|
const log = api.logger('government');
|
|
@@ -839,14 +841,57 @@ async function docketFor(builder) {
|
|
|
839
841
|
// nothing — the docket then says `routed: false` rather than guess whose it is.
|
|
840
842
|
async function loadSeats(government) {
|
|
841
843
|
try {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
+
// task 1005000: who holds each outside access. Its own read fails OPEN to nobody
|
|
845
|
+
// recorded (every access seat then falls to the owner), so a missing table on an
|
|
846
|
+
// instance that has not migrated yet never stops the City routing. Started inside
|
|
847
|
+
// a .then so a db layer without the function (an older stub, the walk's store)
|
|
848
|
+
// fails open too rather than throwing before the catch is attached.
|
|
849
|
+
const accessRows = Promise.resolve().then(() => db.listAccessHolders()).catch((err) => {
|
|
850
|
+
log.warn({ err: err && err.message }, 'governor docket: access holders unreadable, access seats fall to the owner');
|
|
851
|
+
return [];
|
|
852
|
+
});
|
|
853
|
+
const [leads, archons, held] = await Promise.all([db.listLeads(), db.listActiveArchons(), accessRows]);
|
|
854
|
+
return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {}, access: access.holdersByKey(held) };
|
|
844
855
|
} catch (err) {
|
|
845
856
|
log.warn({ err: err && err.message }, 'governor docket: seats unreadable, nothing routed');
|
|
846
857
|
return null;
|
|
847
858
|
}
|
|
848
859
|
}
|
|
849
860
|
|
|
861
|
+
// OPEN BLOCKERS UNDER THE PEOPLE THEY WAIT ON (task 1004999). The blockers source is
|
|
862
|
+
// read exactly as the docket reads it (the ideas module's contribution, under its own
|
|
863
|
+
// `anyBuilder` gate — the list is public), each one carries its stored ask, and the
|
|
864
|
+
// seats are read once, uncached (ADR 0016). waiting.js groups them with routing.js's
|
|
865
|
+
// rule, so this answers "whose is it" exactly as the City does. A board seat's holders
|
|
866
|
+
// are read only when some blocker names one. → { available, people, nobody, yours }
|
|
867
|
+
// `available: false` names why nothing could be grouped — never an empty list
|
|
868
|
+
// standing in for "nobody is waiting".
|
|
869
|
+
async function blockersByPerson(builder, { contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
|
|
870
|
+
const c = contributions.sources.get('blockers');
|
|
871
|
+
if (!c) return { available: false, reason: 'off', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
872
|
+
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
873
|
+
const held = new Set(permissions || []);
|
|
874
|
+
const ctx = Object.freeze({ builderId: String(builder.id), rank: builder.rank || null, has: (p) => held.has(p) });
|
|
875
|
+
if (!gateAllows(c.gate, ctx)) return { available: false, reason: 'not_yours', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
876
|
+
let matters;
|
|
877
|
+
try {
|
|
878
|
+
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
|
|
879
|
+
matters = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source: 'blockers', building: c.building || 'planning' })).filter(Boolean);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
log.warn({ err: err && err.message }, 'blockers by person: blockers unreadable');
|
|
882
|
+
return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
883
|
+
}
|
|
884
|
+
await attachAsks(matters, asksDb.asksFor, timeoutMs);
|
|
885
|
+
const boardSeat = matters.some((m) => /^board:/.test(routing.seatOf(m)));
|
|
886
|
+
const government = boardSeat
|
|
887
|
+
? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
|
|
888
|
+
: { holders: {} };
|
|
889
|
+
const seats = await loadSeats(government);
|
|
890
|
+
if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
|
|
891
|
+
const grouped = waiting.groupByPerson(matters, seats);
|
|
892
|
+
return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
|
|
893
|
+
}
|
|
894
|
+
|
|
850
895
|
// One matter with its stored ask and the seats, for passing it on to `toSeat`
|
|
851
896
|
// (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
|
|
852
897
|
// board's holders are read only when a board seat is involved.
|
|
@@ -927,5 +972,6 @@ module.exports = {
|
|
|
927
972
|
findMatter,
|
|
928
973
|
findMatterFor,
|
|
929
974
|
loadSeats,
|
|
975
|
+
blockersByPerson,
|
|
930
976
|
matterWithSeats,
|
|
931
977
|
};
|
|
@@ -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;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- government_036_access_holders.sql — who holds the keys OUTSIDE Bongos, and an ask
|
|
2
|
+
-- that waits on them (task 1005000, goal 1000111).
|
|
3
|
+
--
|
|
4
|
+
-- WHAT THIS ADDS
|
|
5
|
+
--
|
|
6
|
+
-- government_access_holders — one row per (outside access, builder): the GitHub
|
|
7
|
+
-- repository's admin, the server, the domain's DNS, the hosting account, package
|
|
8
|
+
-- publishing, payments. The vocabulary is modules/government/access.js `ACCESS`,
|
|
9
|
+
-- held equal to the CHECK below by tests/access_holders.mjs. Written by the owner
|
|
10
|
+
-- only (the routes gate government.manage); who granted it and when ride the row.
|
|
11
|
+
--
|
|
12
|
+
-- It is a RECORD OF WHO TO ASK, never an authority grant: no permission atom, rank
|
|
13
|
+
-- check or gate reads this table, and it stores no credential — only who holds one.
|
|
14
|
+
--
|
|
15
|
+
-- government_asks.waits_on gains the seat `access:<key>`, so an ask can wait on
|
|
16
|
+
-- whoever holds an outside access (routing.js; nobody recorded → the owner).
|
|
17
|
+
|
|
18
|
+
BEGIN;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS government_access_holders (
|
|
21
|
+
access_key text NOT NULL
|
|
22
|
+
CHECK (access_key IN ('github-admin', 'server-ssh', 'dns', 'hosting', 'npm-publish', 'payments')),
|
|
23
|
+
builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE CASCADE,
|
|
24
|
+
granted_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
25
|
+
granted_at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
PRIMARY KEY (access_key, builder_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
ALTER TABLE government_asks DROP CONSTRAINT IF EXISTS government_asks_waits_on_check;
|
|
30
|
+
ALTER TABLE government_asks ADD CONSTRAINT government_asks_waits_on_check
|
|
31
|
+
CHECK (waits_on ~ '^(owner|(lead|board):(charter|rules|disputes|people|planning|automation|review|release|treasury)|committee:(charter|rules|disputes|people|planning|automation|review|release|treasury)\.[a-z][a-z0-9_]{0,31}|access:(github-admin|server-ssh|dns|hosting|npm-publish|payments))$');
|
|
32
|
+
|
|
33
|
+
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." },
|