@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// tests/cost_write_guard.mjs — POST /cost refuses what a builder may not book
|
|
2
|
+
// (task 1004959, security report 1000029).
|
|
3
|
+
//
|
|
4
|
+
// The real route runs over a stubbed doorway: requireBuilder admits a builder
|
|
5
|
+
// whose id and rank the test sets, the pool answers the claims lookup from a
|
|
6
|
+
// set of (task, builder) pairs, and logCost is a recorder injected through the
|
|
7
|
+
// router's deps. So every case goes through the route's own wiring (the guard,
|
|
8
|
+
// the claims SQL, the error envelope), not a hand-built input to the guard.
|
|
9
|
+
//
|
|
10
|
+
// The legitimate callers are pinned by the payloads they really send:
|
|
11
|
+
// session-cost.js (session:* rows, and cron:* rows on a scheduled run),
|
|
12
|
+
// ship-grade-steps.js (source 'grader' on the caller's own task), cost.js
|
|
13
|
+
// (manual, optional --task-id) and backfill-cost.js (art_pipeline, no task).
|
|
14
|
+
//
|
|
15
|
+
// Run: node --test tests/cost_write_guard.mjs
|
|
16
|
+
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const express = require('express');
|
|
24
|
+
|
|
25
|
+
const apiPath = require.resolve('../src/module-api.js');
|
|
26
|
+
const realApi = require(apiPath);
|
|
27
|
+
|
|
28
|
+
let caller = { id: 7, rank: 'xenos' };
|
|
29
|
+
let claimed = new Set(); // "taskId:builderId"
|
|
30
|
+
let logged = [];
|
|
31
|
+
const queries = [];
|
|
32
|
+
|
|
33
|
+
const pool = {
|
|
34
|
+
async query(sql, params) {
|
|
35
|
+
queries.push({ sql, params });
|
|
36
|
+
if (/FROM claims/.test(sql)) {
|
|
37
|
+
return { rows: claimed.has(`${params[0]}:${params[1]}`) ? [{ '?column?': 1 }] : [] };
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`unscripted query: ${sql.trim().slice(0, 60)}`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
require.cache[apiPath].exports = {
|
|
43
|
+
...realApi,
|
|
44
|
+
pool,
|
|
45
|
+
requireBuilder: (req, _res, next) => { req.builder = { ...caller }; next(); },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const buildCostRouter = require('../modules/economy/routes/cost.js');
|
|
49
|
+
const { MAX_ROW_USD } = require('../modules/economy/cost-write-guard.js');
|
|
50
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use(express.json());
|
|
54
|
+
app.use(attachFail);
|
|
55
|
+
app.use('/api/bongos', buildCostRouter({
|
|
56
|
+
logCost: async (row) => { logged.push(row); return { id: logged.length, recorded_at: new Date().toISOString() }; },
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
let server, port;
|
|
60
|
+
before(async () => {
|
|
61
|
+
server = http.createServer(app);
|
|
62
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
63
|
+
port = server.address().port;
|
|
64
|
+
});
|
|
65
|
+
after(async () => { await new Promise((r) => server.close(r)); });
|
|
66
|
+
beforeEach(() => { caller = { id: 7, rank: 'xenos' }; claimed = new Set(); logged = []; queries.length = 0; });
|
|
67
|
+
|
|
68
|
+
function post(body) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const payload = JSON.stringify(body);
|
|
71
|
+
const req = http.request({
|
|
72
|
+
host: '127.0.0.1', port, path: '/api/bongos/cost', method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
74
|
+
}, (res) => {
|
|
75
|
+
let data = '';
|
|
76
|
+
res.on('data', (c) => { data += c; });
|
|
77
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data ? JSON.parse(data) : null }));
|
|
78
|
+
});
|
|
79
|
+
req.on('error', reject);
|
|
80
|
+
req.end(payload);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const errCode = (r) => r.body && (r.body.error?.code || r.body.error || r.body.code);
|
|
85
|
+
|
|
86
|
+
// --- the three attacks from the report -------------------------------------
|
|
87
|
+
|
|
88
|
+
test('an agent: row is refused from every rank, Archon included (only the server writes them)', async () => {
|
|
89
|
+
for (const rank of ['xenos', 'metic', 'archon']) {
|
|
90
|
+
caller = { id: 7, rank };
|
|
91
|
+
const r = await post({ amount_usd: 50, category: 'api', source: 'agent:anything', source_ref: `x-${rank}` });
|
|
92
|
+
assert.equal(r.status, 403, `${rank}: agent spend must not come through HTTP`);
|
|
93
|
+
assert.equal(errCode(r), 'reserved_source');
|
|
94
|
+
}
|
|
95
|
+
assert.equal(logged.length, 0, 'nothing reached the ledger');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a market-watch: row is refused from every rank too (the weekly run writes it in-process, task 1004937)', async () => {
|
|
99
|
+
for (const rank of ['xenos', 'metic', 'archon']) {
|
|
100
|
+
caller = { id: 7, rank };
|
|
101
|
+
const r = await post({ amount_usd: 5, category: 'api', source: 'Market-Watch:12', source_ref: `item-${rank}` });
|
|
102
|
+
assert.equal(r.status, 403, `${rank}: Market Watch spend must not come through HTTP`);
|
|
103
|
+
assert.equal(errCode(r), 'reserved_source');
|
|
104
|
+
}
|
|
105
|
+
assert.equal(logged.length, 0, 'nothing reached the ledger');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('the prefix check ignores case and leading spaces', async () => {
|
|
109
|
+
const r = await post({ amount_usd: 50, category: 'api', source: ' AGENT:x', source_ref: 'x1' });
|
|
110
|
+
assert.equal(r.status, 403);
|
|
111
|
+
assert.equal(logged.length, 0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a cron: row is refused below Metic and accepted from Metic+', async () => {
|
|
115
|
+
for (const rank of ['xenos', 'thetes']) {
|
|
116
|
+
caller = { id: 7, rank };
|
|
117
|
+
const r = await post({ amount_usd: 2, category: 'api', source: 'cron:nightly:interactive', source_ref: `s-${rank}` });
|
|
118
|
+
assert.equal(r.status, 403, `${rank} may not book scheduled-run spend`);
|
|
119
|
+
assert.equal(errCode(r), 'reserved_source');
|
|
120
|
+
}
|
|
121
|
+
assert.equal(logged.length, 0);
|
|
122
|
+
for (const rank of ['metic', 'archon']) {
|
|
123
|
+
caller = { id: 7, rank };
|
|
124
|
+
const r = await post({ amount_usd: 2, category: 'api', source: 'cron:nightly:interactive', source_ref: `s-${rank}` });
|
|
125
|
+
assert.equal(r.status, 201, `${rank} runs the scheduled jobs and must still log them`);
|
|
126
|
+
}
|
|
127
|
+
assert.equal(logged.length, 2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a task the caller never claimed is refused; one they claimed (even released) is booked', async () => {
|
|
131
|
+
const r1 = await post({ task_id: 555, amount_usd: 1, category: 'api', source: 'grader', source_ref: 'g1' });
|
|
132
|
+
assert.equal(r1.status, 403);
|
|
133
|
+
assert.equal(errCode(r1), 'task_not_yours');
|
|
134
|
+
assert.equal(logged.length, 0);
|
|
135
|
+
const q = queries.find((x) => /FROM claims/.test(x.sql));
|
|
136
|
+
assert.deepEqual(q.params, [555, 7], 'the lookup is keyed on the task AND the authed caller');
|
|
137
|
+
|
|
138
|
+
claimed.add('555:7');
|
|
139
|
+
const r2 = await post({ task_id: 555, amount_usd: 1, category: 'api', source: 'grader', source_ref: 'g2' });
|
|
140
|
+
assert.equal(r2.status, 201);
|
|
141
|
+
assert.equal(logged[0].taskId, 555);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('someone else\'s claim on the task does not let this caller charge it', async () => {
|
|
145
|
+
claimed.add('555:8');
|
|
146
|
+
const r = await post({ task_id: 555, amount_usd: 1, category: 'other', source_ref: 'm1' });
|
|
147
|
+
assert.equal(r.status, 403);
|
|
148
|
+
assert.equal(errCode(r), 'task_not_yours');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('an Archon may log against any task (backfills) without the claims lookup', async () => {
|
|
152
|
+
caller = { id: 3, rank: 'archon' };
|
|
153
|
+
const r = await post({ task_id: 555, amount_usd: 1, category: 'other', source: 'manual', source_ref: 'a1' });
|
|
154
|
+
assert.equal(r.status, 201);
|
|
155
|
+
assert.equal(queries.filter((x) => /FROM claims/.test(x.sql)).length, 0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('one row is capped at MAX_ROW_USD, on the total and on every tier', async () => {
|
|
159
|
+
const over = MAX_ROW_USD + 1;
|
|
160
|
+
for (const field of ['amount_usd', 'fresh_input_usd', 'cache_read_usd', 'cache_write_usd', 'output_usd']) {
|
|
161
|
+
const body = { amount_usd: 1, category: 'api', source: 'session:interactive', source_ref: `c-${field}` };
|
|
162
|
+
body[field] = over;
|
|
163
|
+
const r = await post(body);
|
|
164
|
+
assert.equal(r.status, 400, `${field} over the cap must be refused`);
|
|
165
|
+
}
|
|
166
|
+
const ok = await post({ amount_usd: MAX_ROW_USD, category: 'api', source: 'session:interactive', source_ref: 'c-at' });
|
|
167
|
+
assert.equal(ok.status, 201, 'exactly the cap is allowed');
|
|
168
|
+
assert.ok(MAX_ROW_USD >= 1000, 'the cap stays far above a real session');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the claims lookup leads on task_id, which a full (not partial) index serves', async () => {
|
|
172
|
+
// The only partial claims index is WHERE released_at IS NULL, which this lookup
|
|
173
|
+
// cannot use (a released claim must count). core_263's idx_claims_task_all is the
|
|
174
|
+
// one that keeps it off a table scan; if either side drifts, this fails.
|
|
175
|
+
const fs = require('node:fs');
|
|
176
|
+
const route = fs.readFileSync(new URL('../modules/economy/routes/cost.js', import.meta.url), 'utf8');
|
|
177
|
+
assert.match(route, /FROM claims WHERE task_id = \$1 AND builder_id = \$2/);
|
|
178
|
+
const mig = fs.readFileSync(new URL('../migrations/core_263_closeness_history_indexes.sql', import.meta.url), 'utf8');
|
|
179
|
+
assert.match(mig, /idx_claims_task_all\s+ON claims \(task_id\);/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- the real callers keep working ----------------------------------------
|
|
183
|
+
|
|
184
|
+
test('session-cost.js rows (no task, session:* sources) still book for any rank', async () => {
|
|
185
|
+
for (const source of ['session:interactive', 'session:subagent']) {
|
|
186
|
+
const r = await post({ amount_usd: 12.5, category: 'api', source, source_ref: `sess-${source}`,
|
|
187
|
+
fresh_input_usd: 1, cache_read_usd: 10, cache_write_usd: 1, output_usd: 0.5 });
|
|
188
|
+
assert.equal(r.status, 201, source);
|
|
189
|
+
}
|
|
190
|
+
assert.equal(logged.length, 2);
|
|
191
|
+
assert.equal(logged[0].builderId, 7, 'attributed to the authed caller');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('the art backfill and a plain manual row (no task) still book', async () => {
|
|
195
|
+
const art = await post({ amount_usd: 0.04, category: 'art', source: 'art_pipeline', source_ref: 'art-1', recorded_at: '2026-10-01T00:00:00Z' });
|
|
196
|
+
assert.equal(art.status, 201);
|
|
197
|
+
const manual = await post({ amount_usd: 3, category: 'other', source_ref: 'man-1' });
|
|
198
|
+
assert.equal(manual.status, 201);
|
|
199
|
+
assert.equal(logged[1].source, 'manual');
|
|
200
|
+
});
|
|
@@ -114,10 +114,16 @@ function fakeLedger({ failReconcile = false } = {}) {
|
|
|
114
114
|
rowCount: 1,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
if (q.startsWith('SELECT
|
|
117
|
+
if (q.startsWith('SELECT builder_id, MAX(id) AS last_id FROM credit_log')) {
|
|
118
|
+
// the backfill read: craft-paid builders with a row newer than the watermark ($5)
|
|
118
119
|
const pats = params.slice(0, 4).map(likeRe);
|
|
119
|
-
const
|
|
120
|
-
|
|
120
|
+
const since = Number(params[4]);
|
|
121
|
+
const last = new Map();
|
|
122
|
+
for (const r of ledger) {
|
|
123
|
+
if (r.id > since && r.delta > 0 && pats.some((re) => re.test(r.reason))) last.set(r.builder_id, Math.max(last.get(r.builder_id) || 0, r.id));
|
|
124
|
+
}
|
|
125
|
+
const ids = [...last.keys()].sort().slice(0, Number(params[5]));
|
|
126
|
+
return { rows: ids.map((builder_id) => ({ builder_id, last_id: String(last.get(builder_id)) })) };
|
|
121
127
|
}
|
|
122
128
|
if (q.startsWith('SELECT COUNT(*)::int AS n FROM credit_log')) return { rows: [{ n: 0 }], rowCount: 1 };
|
|
123
129
|
if (q.startsWith('SELECT total_credits FROM builders')) return { rows: [{ total_credits: 0 }], rowCount: 1 };
|
|
@@ -274,7 +280,7 @@ await test('a builder holding every craft badge costs one read and nothing more'
|
|
|
274
280
|
|
|
275
281
|
// ---- 4. the backfill ------------------------------------------------------------------
|
|
276
282
|
|
|
277
|
-
await test('A Whole Surface is backfilled by the
|
|
283
|
+
await test('A Whole Surface is backfilled by the hourly incremental sweep, for the artists the ledger has paid', async () => {
|
|
278
284
|
seams._reset();
|
|
279
285
|
let asked = [];
|
|
280
286
|
seams.registerProvider('copy-desk.artistStats', async (id) => { asked.push(String(id)); return { surfaces: [{ tweaked: 4, total: 4 }] }; });
|
|
@@ -289,17 +295,34 @@ await test('A Whole Surface is backfilled by the once-per-start sweep, for the a
|
|
|
289
295
|
assert.deepEqual(asked, ['2001'], 'the studio is asked about the artist only');
|
|
290
296
|
const again = await achievements.backfillCraftAchievements({ client: c });
|
|
291
297
|
assert.equal(again.unlocked, 0, 'a second run unlocks nothing');
|
|
298
|
+
assert.equal(out.watermark, 1001, 'the watermark is the highest CRAFT credit_log id covered (pay() numbers rows from 1000; the engineer row 1002 is not craft)');
|
|
299
|
+
|
|
300
|
+
// task 1004965: INCREMENTAL — a caught-up run reads only past the watermark and checks nobody
|
|
301
|
+
const reads = () => c.sql.filter((q) => q.startsWith('SELECT builder_id, MAX(id) AS last_id FROM credit_log'));
|
|
302
|
+
const before = c.sql.length;
|
|
303
|
+
const caughtUp = await achievements.backfillCraftAchievements({ client: c, sinceId: out.watermark });
|
|
304
|
+
assert.deepEqual(caughtUp, { checked: 0, unlocked: 0, watermark: out.watermark }, 'caught up: nobody re-checked, watermark held');
|
|
305
|
+
assert.equal(c.sql.length - before, 1, 'one statement in all — no per-builder reads');
|
|
306
|
+
assert.match(reads().at(-1), /WHERE id > \$5 AND/, 'bounded by the watermark, never a full scan');
|
|
307
|
+
pay(c, '2003', 'idea.credit.task_ship:8:102');
|
|
308
|
+
const fresh = await achievements.backfillCraftAchievements({ client: c, sinceId: out.watermark });
|
|
309
|
+
assert.equal(fresh.checked, 1, 'only the builder paid since the watermark');
|
|
310
|
+
assert.ok(fresh.watermark > out.watermark, 'and the watermark moves past them');
|
|
311
|
+
const capped = await achievements.backfillCraftAchievements({ client: c, limit: 1 });
|
|
312
|
+
assert.equal(capped.watermark, 0, 'a run that hit its limit keeps the old watermark, skipping nobody past it');
|
|
292
313
|
|
|
293
314
|
const reconciler = require('../modules/lifecycle/publish-reconciler.js');
|
|
294
|
-
const
|
|
295
|
-
const reward = { backfillCraftAchievements: async () => {
|
|
315
|
+
const sinceAsked = [];
|
|
316
|
+
const reward = { backfillCraftAchievements: async (o) => { sinceAsked.push(o.sinceId); return { checked: 1, unlocked: 1, watermark: 42 }; } };
|
|
296
317
|
const quiet = { logFn: () => {}, errFn: () => {} };
|
|
297
|
-
await reconciler.runCraftAchievementBackfill({ reward, ...quiet, forceCraftBackfill: true });
|
|
298
318
|
await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
|
|
299
|
-
|
|
300
|
-
|
|
319
|
+
await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
|
|
320
|
+
assert.deepEqual(sinceAsked, [0, 42], 'the first run scans from 0; the next starts at the watermark it was handed');
|
|
321
|
+
const failing = await reconciler.runCraftAchievementBackfill({ reward: { backfillCraftAchievements: async () => { throw new Error('db down'); } }, ...quiet });
|
|
301
322
|
assert.equal(failing.skipped, 'error', 'a failure is logged and swallowed');
|
|
302
|
-
|
|
323
|
+
await reconciler.runCraftAchievementBackfill({ reward, ...quiet });
|
|
324
|
+
assert.equal(sinceAsked.at(-1), 42, 'a failed run keeps the watermark');
|
|
325
|
+
assert.match(read('modules/lifecycle/publish-reconciler.js'), /\['craft achievement backfill', runCraftAchievementBackfill, HOURLY_MS\]/, 'the tick runs it HOURLY (an entry in the guarded SWEEPS list, tasks 1004924 + 1004965)');
|
|
303
326
|
seams._reset();
|
|
304
327
|
});
|
|
305
328
|
|
|
@@ -562,7 +562,7 @@ await test('setGoalStatus writes on the transaction client (task 1003840)', asyn
|
|
|
562
562
|
// reintroduce the very pool-vs-transaction deadlock this test guards.
|
|
563
563
|
assert.equal(seen.length, 2, 'the UPDATE and the request sweep reached the injected client');
|
|
564
564
|
assert.match(seen[0].sql, /UPDATE goals/);
|
|
565
|
-
assert.deepEqual(seen[0].params, [3, 'achieved']);
|
|
565
|
+
assert.deepEqual(seen[0].params, [3, 'achieved', null]);
|
|
566
566
|
assert.match(seen[1].sql, /UPDATE goal_membership_requests/);
|
|
567
567
|
});
|
|
568
568
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import assert from 'node:assert/strict';
|
|
15
15
|
import { test } from 'node:test';
|
|
16
|
-
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
@@ -111,6 +111,78 @@ test('it runs the same on macOS, where stat refuses -c and wc pads its numbers (
|
|
|
111
111
|
assert.match(r.stdout, /ok · 1 dump\(s\) retained/);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// task 1004966 — the nightly unit and the hall's "Back up now" both run this script, and
|
|
115
|
+
// the script takes a per-database flock under the backup dir, so a second run while one
|
|
116
|
+
// is dumping exits 0 with a SKIPPED line instead of starting a concurrent pg_dump. Needs
|
|
117
|
+
// flock (util-linux): skipped on Windows and wherever flock is not on PATH.
|
|
118
|
+
const hasFlock = process.platform !== 'win32' && spawnSync('bash', ['-c', 'command -v flock'], { encoding: 'utf8' }).status === 0;
|
|
119
|
+
const lockSkip = skip || (hasFlock ? false : 'needs flock (util-linux; runs in CI)');
|
|
120
|
+
|
|
121
|
+
// A slow fake pg_dump that counts its starts, so a test can see whether a second run
|
|
122
|
+
// began a dump while the first was still inside one.
|
|
123
|
+
function slowBackupDir() {
|
|
124
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-backup-lock-'));
|
|
125
|
+
const bin = path.join(tmp, 'bin');
|
|
126
|
+
const dir = path.join(tmp, 'backups');
|
|
127
|
+
fs.mkdirSync(bin);
|
|
128
|
+
fs.mkdirSync(dir);
|
|
129
|
+
const starts = path.join(tmp, 'starts');
|
|
130
|
+
fs.writeFileSync(path.join(bin, 'pg_dump'), [
|
|
131
|
+
'#!/bin/bash',
|
|
132
|
+
`echo started >> "${starts}"`,
|
|
133
|
+
'sleep 3',
|
|
134
|
+
"printf -- '--\\n-- PostgreSQL database dump\\n--\\n--\\n-- PostgreSQL database dump complete\\n--\\n'",
|
|
135
|
+
'',
|
|
136
|
+
].join('\n'), { mode: 0o755 });
|
|
137
|
+
const env = (db) => ({ ...process.env, PATH: `${bin}:${process.env.PATH}`, [`${ENV}DB`]: db, [`${ENV}DIR`]: dir });
|
|
138
|
+
const startCount = () => (fs.existsSync(starts) ? fs.readFileSync(starts, 'utf8').trim().split('\n').length : 0);
|
|
139
|
+
return { dir, env, startCount };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function waitFor(pred, ms = 5000) {
|
|
143
|
+
const until = Date.now() + ms;
|
|
144
|
+
while (!pred()) {
|
|
145
|
+
if (Date.now() > until) throw new Error('timed out waiting for the first run to start its dump');
|
|
146
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function runAsync(env) {
|
|
151
|
+
const child = spawn('bash', [SCRIPT], { env });
|
|
152
|
+
let stdout = '';
|
|
153
|
+
child.stdout.on('data', (c) => { stdout += c; });
|
|
154
|
+
child.stderr.resume();
|
|
155
|
+
return new Promise((resolve) => child.on('close', (status) => resolve({ status, stdout })));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('a second run while one is dumping does not start a concurrent dump (task 1004966)', { skip: lockSkip }, async () => {
|
|
159
|
+
const { dir, env, startCount } = slowBackupDir();
|
|
160
|
+
const first = runAsync(env('smallproj'));
|
|
161
|
+
await waitFor(() => startCount() === 1);
|
|
162
|
+
const second = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('smallproj') });
|
|
163
|
+
assert.equal(second.status, 0, 'the second run exits cleanly');
|
|
164
|
+
assert.match(second.stdout, /gds-db-backup: SKIPPED another backup of smallproj is already running/);
|
|
165
|
+
assert.equal(startCount(), 1, 'the second run never started pg_dump');
|
|
166
|
+
const r = await first;
|
|
167
|
+
assert.equal(r.status, 0);
|
|
168
|
+
assert.equal(fs.readdirSync(dir).filter((f) => f.endsWith('.sql.gz')).length, 1, 'exactly one dump landed');
|
|
169
|
+
// Released on exit: the next run dumps normally.
|
|
170
|
+
const third = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('smallproj') });
|
|
171
|
+
assert.equal(third.status, 0, third.stderr);
|
|
172
|
+
assert.equal(startCount(), 2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('the lock is per database: another project sharing the backup dir still dumps', { skip: lockSkip }, async () => {
|
|
176
|
+
const { env, startCount } = slowBackupDir();
|
|
177
|
+
const first = runAsync(env('smallproj'));
|
|
178
|
+
await waitFor(() => startCount() === 1);
|
|
179
|
+
const other = spawnSync('bash', [SCRIPT], { encoding: 'utf8', env: env('otherproj') });
|
|
180
|
+
assert.equal(other.status, 0, other.stderr);
|
|
181
|
+
assert.doesNotMatch(other.stdout, /SKIPPED/);
|
|
182
|
+
assert.equal(startCount(), 2);
|
|
183
|
+
assert.equal((await first).status, 0);
|
|
184
|
+
});
|
|
185
|
+
|
|
114
186
|
test('no fixed size floor is baked in for every instance', () => {
|
|
115
187
|
const src = fs.readFileSync(SCRIPT, 'utf8');
|
|
116
188
|
assert.doesNotMatch(src, new RegExp(`${ENV}MIN_BYTES:-[1-9]`), 'a default floor fails every database smaller than it');
|
|
@@ -84,6 +84,13 @@ test('the line says who sees a merge, not a blanket rule in either direction', (
|
|
|
84
84
|
assert.match(s, /Never call work live until that instance's \/version shows it/);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
test('the line tells the agent not to relay it to the builder (task 1004952)', () => {
|
|
88
|
+
// Owner, 2026-10-28: agents repeated this note in replies every session.
|
|
89
|
+
const s = line(behind);
|
|
90
|
+
assert.match(s, /This note is for you, not the builder: do not repeat it to them unless they ask whether work is live/);
|
|
91
|
+
assert.ok(!s.includes('\n'), 'still one line');
|
|
92
|
+
});
|
|
93
|
+
|
|
87
94
|
test('the line says whether main\'s latest merge is in a release yet', () => {
|
|
88
95
|
// task 1004873: the first question behind "is it live?" is "is it released?".
|
|
89
96
|
const none = line({ ...behind, behind: false, hasUnreleased: true, unreleased: { count: 4 }, tip: { short: '0f39e344', version: null } });
|
|
@@ -160,6 +160,18 @@ const NON_PUBLIC_READS = [
|
|
|
160
160
|
fn: 'getAccountByLogin',
|
|
161
161
|
why: 'resolves a github_login for the Archon-gated invite path (POST /projects/invite). Inviting a person is not publishing them: the caller already knows the login and the result never reaches a public body.',
|
|
162
162
|
},
|
|
163
|
+
{
|
|
164
|
+
fn: 'findPersonByLogin',
|
|
165
|
+
why: 'the sign-in lookup (task 1004992, ADR 0393): which account a provider identity reaches, so a sign-in through Google lands on the right person. It returns only the account id and person key to the sign-in route that minted the session; nothing it returns reaches a public body, and gating it on visibility would lock a private account out of its own sign-in.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
fn: 'signInWithLogin',
|
|
169
|
+
why: 'the Google sign-in (task 1004993, ADR 0396): after finding or creating the person by the provider id, it reads their handle and any linked GitHub login for the core sign-in route that mints their session. Nothing it returns reaches a public body, and a private account must still be able to sign in.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
fn: 'listLoginsForPerson',
|
|
173
|
+
why: 'the own-scope read behind GET /me/sign-in-methods (task 1004992): which ways in the signed-in person has, keyed on their authenticated key only. It returns their own providers and addresses to themselves; a private account must still see its own sign-in methods.',
|
|
174
|
+
},
|
|
163
175
|
{
|
|
164
176
|
fn: 'hiddenGithubIds',
|
|
165
177
|
why: 'the closeness-graph consent gate — it answers only "which of these accounts set hide_stats", the OPPOSITE question to visibility. Its callers are requireBuilder-gated and it must stay fail-closed on hide alone (ADR 0171 D4); joining addressability would silently empty the graph of every private account for readers who are allowed to see the edge.',
|
package/tests/fitness.mjs
CHANGED
|
@@ -74,6 +74,25 @@ test('the live CLAUDE.md delegates the charter to the rule file', () => {
|
|
|
74
74
|
assert.ok(kernel.includes(CHARTER_RULE_REL), 'the heading must point somewhere, or it is decoration');
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
// task 1004933 trimmed quick-nav rows that only repeated a section's own pointer.
|
|
78
|
+
// The pointer itself must survive somewhere in the kernel, and still resolve.
|
|
79
|
+
test('the kernel still points at every detail file a trimmed quick-nav row named', () => {
|
|
80
|
+
const kernel = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
|
|
81
|
+
for (const rel of ['docs/architecture.md', 'docs/file-map.md', 'docs/canonical-permissions.md',
|
|
82
|
+
'docs/handoff-template.md', 'docs/project-context.template.md', 'config/branding.neutral.json', 'docs/branding-contract.md']) {
|
|
83
|
+
assert.ok(kernel.includes(`](${rel})`), `CLAUDE.md lost its link to ${rel}`);
|
|
84
|
+
assert.ok(fs.existsSync(path.join(ROOT, rel)), `${rel} is linked but missing`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('every section rule in the kernel sits after a blank line (a trim must not make a heading)', () => {
|
|
89
|
+
// Text directly above a `---` is a Setext H2 in CommonMark: the paragraph becomes a
|
|
90
|
+
// heading and the section break disappears. A substring check on links stays green.
|
|
91
|
+
const lines = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8').split(/\r?\n/);
|
|
92
|
+
const glued = lines.flatMap((l, i) => (l === '---' && i > 0 && lines[i - 1].trim() !== '' ? [i + 1] : []));
|
|
93
|
+
assert.deepEqual(glued, [], `CLAUDE.md has a --- with text directly above it at line(s) ${glued.join(', ')}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
77
96
|
test('a hollow charter heading hard-fails (the heading alone is not the charter)', () => {
|
|
78
97
|
const r = fitness.checkClaudeMdBudget({ text: `${CHARTER_HEADING}\n` });
|
|
79
98
|
assert.equal(r.hardFail, true, 'a heading with neither inline rules nor a pointer must fail');
|