@bongos/core 1.23.22 → 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 +902 -407
- 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 +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -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/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 +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- 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 +1 -0
- package/docs/module-api-changelog.md +5 -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/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/catalog.js +6 -0
- package/modules/government/db.js +34 -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 +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -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 +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/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-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 +220 -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 +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 +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/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_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- 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
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
|
|
@@ -1114,6 +1131,21 @@ async function getExpiredOpenBoardItems(limit = 100) {
|
|
|
1114
1131
|
return rows;
|
|
1115
1132
|
}
|
|
1116
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
|
+
|
|
1117
1149
|
// The summons read (task 1003271): every OPEN item this builder could still
|
|
1118
1150
|
// vote on, with the two facts that decide it — who authored the subject, and
|
|
1119
1151
|
// whether this builder has already cast. Membership is NOT decided here: it
|
|
@@ -1260,6 +1292,8 @@ module.exports = {
|
|
|
1260
1292
|
withdrawBoardItem,
|
|
1261
1293
|
getIdeaCompleteness,
|
|
1262
1294
|
getExpiredOpenBoardItems,
|
|
1295
|
+
msUntilSoonestOpenDeadline,
|
|
1296
|
+
onBoardDeadline,
|
|
1263
1297
|
getLatestReturnedItemWithObjection,
|
|
1264
1298
|
getLatestPassedItem,
|
|
1265
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." },
|
|
@@ -767,6 +767,28 @@ module.exports = function buildGovernmentRouter() {
|
|
|
767
767
|
// treasury; `rule` ∈ decide first consent majority unanimous signoff rule (or
|
|
768
768
|
// null); `state` ∈ needs moving settled. Nothing is ever sample data: a source
|
|
769
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
|
+
|
|
770
792
|
router.get('/government/docket', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
771
793
|
try {
|
|
772
794
|
res.json(await docket.docketFor(req.builder));
|
|
@@ -69,6 +69,35 @@ const idsOf = (list) => (Array.isArray(list) ? list : [])
|
|
|
69
69
|
.filter((id) => id != null)
|
|
70
70
|
.map(String);
|
|
71
71
|
|
|
72
|
+
// Who each builder id is, from the seats already read (task 1004999): the owner
|
|
73
|
+
// seat's Archons, the Leads in office and each building's holders. A route names
|
|
74
|
+
// people, not just a seat — "waits on Lars, the owner", not "waits on the owner".
|
|
75
|
+
// Pure; an id no seat row names reads as { id, login: null, name: null }.
|
|
76
|
+
function directoryOf(ctx) {
|
|
77
|
+
const c = ctx || {};
|
|
78
|
+
const dir = new Map();
|
|
79
|
+
const put = (id, login, name) => {
|
|
80
|
+
if (id == null) return;
|
|
81
|
+
const key = String(id);
|
|
82
|
+
const prev = dir.get(key);
|
|
83
|
+
if (prev && prev.name) return;
|
|
84
|
+
dir.set(key, { id: key, login: login || null, name: name || login || null });
|
|
85
|
+
};
|
|
86
|
+
for (const b of (c.owner && c.owner.builders) || []) put(b.id, b.login, b.name);
|
|
87
|
+
for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
|
|
88
|
+
if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
|
|
89
|
+
}
|
|
90
|
+
for (const list of Object.values(c.holders || {})) {
|
|
91
|
+
for (const p of Array.isArray(list) ? list : []) {
|
|
92
|
+
if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return dir;
|
|
96
|
+
}
|
|
97
|
+
function peopleOf(ids, dir) {
|
|
98
|
+
return (ids || []).map((id) => dir.get(String(id)) || { id: String(id), login: null, name: null });
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
// resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
|
|
73
102
|
// asked the seat the matter names
|
|
74
103
|
// seat the seat that answers it, after any fall
|
|
@@ -97,11 +126,13 @@ function resolveSeat(asked, ctx) {
|
|
|
97
126
|
return toOwner();
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
// routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
|
|
101
|
-
// { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
|
|
129
|
+
// routeMatter(matter, viewerId, ctx, now, dir) → the route the docket carries:
|
|
130
|
+
// { seat, label, asked, fell_to_owner, people, yours, nobody: null | { why, outcome } }
|
|
102
131
|
// `yours` is the one answer to "is this on me": an open matter that needs a
|
|
103
132
|
// person, on a seat the viewer sits in, that somebody can still answer.
|
|
104
|
-
|
|
133
|
+
// `dir` is directoryOf(ctx); routeAll builds it ONCE per request and passes it,
|
|
134
|
+
// so a docket of N matters does not rebuild the same name map N times.
|
|
135
|
+
function routeMatter(matter, viewerId, ctx, now = Date.now(), dir = directoryOf(ctx)) {
|
|
105
136
|
const m = matter || {};
|
|
106
137
|
const needs = m.state === 'needs';
|
|
107
138
|
// a sitting's voters are the item's own (its snapshot), never a fall to the owner
|
|
@@ -120,6 +151,8 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
120
151
|
label: r.label,
|
|
121
152
|
asked: r.asked,
|
|
122
153
|
fell_to_owner: r.fell_to_owner,
|
|
154
|
+
// who sits in that seat now, by name (task 1004999); [] for a sitting's own voters
|
|
155
|
+
people: peopleOf(r.builder_ids, dir),
|
|
123
156
|
yours: needs && !nobody && !!viewer && sits,
|
|
124
157
|
nobody,
|
|
125
158
|
};
|
|
@@ -128,8 +161,9 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
|
128
161
|
// Route every matter in place; → how many are the viewer's.
|
|
129
162
|
function routeAll(matters, viewerId, ctx, now = Date.now()) {
|
|
130
163
|
let yours = 0;
|
|
164
|
+
const dir = directoryOf(ctx);
|
|
131
165
|
for (const m of matters || []) {
|
|
132
|
-
m.route = routeMatter(m, viewerId, ctx, now);
|
|
166
|
+
m.route = routeMatter(m, viewerId, ctx, now, dir);
|
|
133
167
|
if (m.route.yours) yours += 1;
|
|
134
168
|
}
|
|
135
169
|
return yours;
|
|
@@ -151,4 +185,4 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
|
|
|
151
185
|
return null;
|
|
152
186
|
}
|
|
153
187
|
|
|
154
|
-
module.exports = { seatOf, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
188
|
+
module.exports = { seatOf, resolveSeat, directoryOf, peopleOf, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/waiting.js — open blockers grouped UNDER THE PEOPLE they wait on
|
|
4
|
+
// (task 1004999, goal 1000111).
|
|
5
|
+
//
|
|
6
|
+
// WHY. A blocker has no person on it: the blockers table has no assignee, and the only
|
|
7
|
+
// "who" is the seat its ask names (owner | lead:<b> | board:<b> | committee:…). So every
|
|
8
|
+
// surface listed every blocker to everyone, /blocker-review framed them all as the
|
|
9
|
+
// owner's, and nobody could open one place and see "these wait on me" — or see that a
|
|
10
|
+
// blocker was waiting on Lars rather than on "the owner".
|
|
11
|
+
//
|
|
12
|
+
// THE RULE. The seat is the truth; the person is resolved at READ time with the same
|
|
13
|
+
// routing the City uses (routing.js seatOf → resolveSeat), so the two can never
|
|
14
|
+
// disagree about whose a blocker is, and seating a new Lead moves their blockers with
|
|
15
|
+
// no blocker edited. Never copy a name onto a blocker.
|
|
16
|
+
//
|
|
17
|
+
// A blocker waits on EVERY person its seat resolves to (a board's members, or two
|
|
18
|
+
// Archons as the owner), so it is listed under each of them. One that resolves to
|
|
19
|
+
// nobody is listed under `nobody`, never dropped.
|
|
20
|
+
//
|
|
21
|
+
// PURE: no db. docket.js reads the blockers, their asks and the seats, then calls this.
|
|
22
|
+
|
|
23
|
+
const routing = require('./routing');
|
|
24
|
+
|
|
25
|
+
// One blocker as a line under a person: what it is, where it lives, and the seat it
|
|
26
|
+
// reaches that person through, in words ("as the owner", "as the Planning Lead").
|
|
27
|
+
function itemOf(m, r) {
|
|
28
|
+
return {
|
|
29
|
+
id: m.ref && m.ref.id != null ? String(m.ref.id) : String(m.id),
|
|
30
|
+
matter_id: String(m.id),
|
|
31
|
+
title: m.title,
|
|
32
|
+
href: m.href || null,
|
|
33
|
+
opened_at: m.opened_at || null,
|
|
34
|
+
where: m.where || null,
|
|
35
|
+
seat: r.seat,
|
|
36
|
+
as: r.label,
|
|
37
|
+
asked: r.asked,
|
|
38
|
+
fell_to_owner: !!r.fell_to_owner,
|
|
39
|
+
question: m.ask && m.ask.question ? String(m.ask.question) : null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const byOpened = (a, b) => String(a.opened_at || '').localeCompare(String(b.opened_at || '')) || a.id.localeCompare(b.id, 'en', { numeric: true });
|
|
44
|
+
|
|
45
|
+
// groupByPerson(matters, seats) → { people: [{ person, count, items }], nobody: [items] }
|
|
46
|
+
// People with the most waiting first, then by name; inside a person, oldest first.
|
|
47
|
+
function groupByPerson(matters, seats) {
|
|
48
|
+
const dir = routing.directoryOf(seats);
|
|
49
|
+
const groups = new Map();
|
|
50
|
+
const nobody = [];
|
|
51
|
+
for (const m of matters || []) {
|
|
52
|
+
if (!m || m.state === 'settled') continue;
|
|
53
|
+
const r = routing.resolveSeat(routing.seatOf(m), seats);
|
|
54
|
+
const item = itemOf(m, r);
|
|
55
|
+
if (!r.builder_ids.length) { nobody.push(item); continue; }
|
|
56
|
+
for (const p of routing.peopleOf(r.builder_ids, dir)) {
|
|
57
|
+
if (!groups.has(p.id)) groups.set(p.id, { person: p, items: [] });
|
|
58
|
+
groups.get(p.id).items.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const people = [...groups.values()]
|
|
62
|
+
.map((g) => ({ person: g.person, count: g.items.length, items: g.items.sort(byOpened) }))
|
|
63
|
+
.sort((a, b) => b.count - a.count || String(a.person.name || a.person.id).localeCompare(String(b.person.name || b.person.id)));
|
|
64
|
+
return { people, nobody: nobody.sort(byOpened) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The viewer's own group: what waits on THEM. → { person, count, items } (count 0 when none).
|
|
68
|
+
function forViewer(grouped, viewerId) {
|
|
69
|
+
const id = viewerId == null ? null : String(viewerId);
|
|
70
|
+
const g = id && grouped && Array.isArray(grouped.people) ? grouped.people.find((x) => x.person.id === id) : null;
|
|
71
|
+
return g || { person: id ? { id, login: null, name: null } : null, count: 0, items: [] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { groupByPerson, forViewer };
|