@bongos/core 1.23.21 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +983 -428
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +6 -3
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +4 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +55 -6
- package/docs/page-readings.json +1185 -1011
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/CLAUDE.md +15 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/module.json +26 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +252 -0
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +108 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +156 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/city.html +15 -15
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +24 -0
- package/modules/lifecycle/db-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/onboarding/howto-moments.js +8 -1
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +8 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +286 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +4 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +129 -12
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +16 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/src/platform-server.js +27 -1
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/batched_writes_parity.mjs +340 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/conductor_main_e2e.mjs +4 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_archive_disposition.mjs +5 -5
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/module_skills_hook.mjs +69 -0
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/public_landing_projects.mjs +37 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +58 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_close_route.mjs +1 -1
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
package/tests/metrics.mjs
CHANGED
|
@@ -100,6 +100,96 @@ await test('httpMiddleware records request + duration + auth-failure on res fini
|
|
|
100
100
|
assert.doesNotMatch(out, /route="\/metrics"/, '/metrics scrape is not self-counted');
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// task 1004924 — the label leak. A scanner crawl of random 404 paths used to mint
|
|
104
|
+
// a new counter AND histogram series per path, held forever.
|
|
105
|
+
await test('a crawl of 404 paths collapses to ONE unmatched series, not one per path', async () => {
|
|
106
|
+
metrics.__resetForTests();
|
|
107
|
+
const mw = metrics.httpMiddleware();
|
|
108
|
+
for (let i = 0; i < 300; i++) {
|
|
109
|
+
const req = fakeReq({ path: `/wp-admin/probe-${i}/x.php`, method: 'GET' });
|
|
110
|
+
const res = new EventEmitter(); res.statusCode = 404;
|
|
111
|
+
mw(req, res, () => {}); res.emit('finish');
|
|
112
|
+
}
|
|
113
|
+
const out = await metrics.render(fakePool);
|
|
114
|
+
assert.match(out, /gds_http_requests_total\{method="GET",route="unmatched",status="404"\} 300/);
|
|
115
|
+
assert.doesNotMatch(out, /probe-/, 'no literal scanner path becomes a label');
|
|
116
|
+
assert.equal((out.match(/^gds_http_requests_total\{/gm) || []).length, 1, 'one counter series');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('a matched route labels by mount path + pattern, never the literal path', async () => {
|
|
120
|
+
metrics.__resetForTests();
|
|
121
|
+
const mw = metrics.httpMiddleware();
|
|
122
|
+
const hit = (req, status) => {
|
|
123
|
+
const res = new EventEmitter(); res.statusCode = status;
|
|
124
|
+
mw({ method: 'GET', ...req }, res, () => {}); res.emit('finish');
|
|
125
|
+
};
|
|
126
|
+
const routed = { baseUrl: '/api/bongos', route: { path: '/tasks/:id' } };
|
|
127
|
+
hit({ path: '/tasks/1380', ...routed }, 200);
|
|
128
|
+
// a matched route that answers 404 (task not found) keeps its own label
|
|
129
|
+
hit({ path: '/tasks/999999', ...routed }, 404);
|
|
130
|
+
// a mount path carrying a literal id is normalized, not kept verbatim
|
|
131
|
+
hit({ path: '/x', baseUrl: '/api/projects/77', route: { path: '/x' } }, 200);
|
|
132
|
+
// unrouted, non-404 (a static file) keeps the normalized path
|
|
133
|
+
hit({ path: '/assets/atlas-89d47b50abcd' }, 200);
|
|
134
|
+
const out = await metrics.render(fakePool);
|
|
135
|
+
assert.match(out, /route="\/api\/bongos\/tasks\/:id",status="200"\} 1/);
|
|
136
|
+
assert.match(out, /route="\/api\/bongos\/tasks\/:id",status="404"\} 1/);
|
|
137
|
+
assert.match(out, /route="\/api\/projects\/:id\/x",status="200"\} 1/);
|
|
138
|
+
assert.match(out, /route="\/assets\/:hash",status="200"\} 1/);
|
|
139
|
+
// Scoped to route labels: a duration _sum such as 0.0000393999999... matched a
|
|
140
|
+
// bare /999999/ and redded this test at random.
|
|
141
|
+
assert.doesNotMatch(out, /route="[^"]*(1380|999999|projects\/77)/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await test('through real Express: matched routes label by pattern, unknown paths are unmatched', async () => {
|
|
145
|
+
const express = require('express');
|
|
146
|
+
metrics.__resetForTests();
|
|
147
|
+
const app = express();
|
|
148
|
+
app.use(metrics.httpMiddleware());
|
|
149
|
+
const router = express.Router();
|
|
150
|
+
router.get('/tasks/:id', (req, res) => res.json({ id: req.params.id }));
|
|
151
|
+
app.use('/api/bongos', router);
|
|
152
|
+
const server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)); });
|
|
153
|
+
try {
|
|
154
|
+
const port = server.address().port;
|
|
155
|
+
// node:http with no keep-alive agent, not fetch: pooled fetch sockets still
|
|
156
|
+
// closing at process.exit abort the run on Windows (libuv assertion).
|
|
157
|
+
const get = (p) => new Promise((resolve, reject) => {
|
|
158
|
+
require('node:http').get({ host: '127.0.0.1', port, path: p, agent: false }, (res) => { res.resume(); res.on('end', resolve); }).on('error', reject);
|
|
159
|
+
});
|
|
160
|
+
for (const p of ['/api/bongos/tasks/1', '/api/bongos/tasks/abc', '/.env', '/wp-login.php', '/random-slug-zz']) await get(p);
|
|
161
|
+
await new Promise((r) => setTimeout(r, 20)); // let every 'finish' land
|
|
162
|
+
} finally { await new Promise((r) => server.close(r)); }
|
|
163
|
+
const out = await metrics.render(fakePool);
|
|
164
|
+
assert.match(out, /route="\/api\/bongos\/tasks\/:id",status="200"\} 2/);
|
|
165
|
+
assert.match(out, /route="unmatched",status="404"\} 3/);
|
|
166
|
+
assert.doesNotMatch(out, /\.env|wp-login|random-slug/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await test('series cap: past the cap a metric folds new label sets into one overflow series', async () => {
|
|
170
|
+
metrics.__resetForTests();
|
|
171
|
+
// The cap's exact value is not the contract; that it exists and is modest is.
|
|
172
|
+
const N = 12000;
|
|
173
|
+
for (let i = 0; i < N; i++) {
|
|
174
|
+
metrics.incCounter('cap_test_total', 'h', { method: 'GET', route: `/static/f${i}.png`, status: '200' });
|
|
175
|
+
metrics.observeHistogram('cap_test_seconds', 'h', { method: 'GET', route: `/static/f${i}.png` }, 0.01);
|
|
176
|
+
metrics.incCounter('cap_noroute_total', 'h', { builder: `b${i}` });
|
|
177
|
+
}
|
|
178
|
+
const out = await metrics.render(fakePool);
|
|
179
|
+
const counterSeries = (out.match(/^cap_test_total\{/gm) || []).length;
|
|
180
|
+
assert.ok(counterSeries > 1 && counterSeries <= 10001, `bounded: ${counterSeries} series for ${N} label sets`);
|
|
181
|
+
const folded = N - (counterSeries - 1);
|
|
182
|
+
// assert.ok, not match: a failing match would dump a multi-megabyte render
|
|
183
|
+
assert.ok(new RegExp(`cap_test_total\\{method="GET",route="other",status="200"\\} ${folded}\\n`).test(out), 'overflow counter series holds every folded hit');
|
|
184
|
+
assert.ok(new RegExp(`cap_test_seconds_count\\{method="GET",route="other"\\} ${folded}\\n`).test(out), 'overflow histogram series');
|
|
185
|
+
// a metric with no route label folds every label to 'other' — still one series
|
|
186
|
+
assert.equal((out.match(/^cap_noroute_total\{/gm) || []).length, counterSeries);
|
|
187
|
+
assert.ok(/cap_noroute_total\{builder="other"\}/.test(out), 'route-less overflow series');
|
|
188
|
+
// an EXISTING series still counts normally once the cap is reached
|
|
189
|
+
metrics.incCounter('cap_test_total', 'h', { method: 'GET', route: '/static/f0.png', status: '200' });
|
|
190
|
+
assert.ok(/cap_test_total\{method="GET",route="\/static\/f0\.png",status="200"\} 2/.test(await metrics.render(fakePool)), 'existing series still counts');
|
|
191
|
+
});
|
|
192
|
+
|
|
103
193
|
await test('authorizeScrape: local Prometheus yes, public no, bearer token yes', () => {
|
|
104
194
|
// loopback, no proxy header → allowed (the local agent)
|
|
105
195
|
assert.equal(metrics.authorizeScrape(fakeReq({ remote: '127.0.0.1' })), true);
|
|
@@ -144,4 +234,38 @@ await test('render is resilient: a DB failure still yields process metrics + db_
|
|
|
144
234
|
assert.match(out, /gds_db_scrape_ok\{?\}? ?0/);
|
|
145
235
|
});
|
|
146
236
|
|
|
237
|
+
// Task 1004961: a 1 s lag timer kept an idle instance awake forever. Load the
|
|
238
|
+
// module fresh with setInterval spied, and pin that every interval it arms is
|
|
239
|
+
// at least 5 s (the audit's allowed 5-10 s sampling window) and unref'd.
|
|
240
|
+
await test('no sub-5-second interval is armed at load (event-loop lag sampler)', async () => {
|
|
241
|
+
const realSetInterval = globalThis.setInterval;
|
|
242
|
+
const armed = [];
|
|
243
|
+
globalThis.setInterval = (fn, ms, ...rest) => {
|
|
244
|
+
const t = realSetInterval(fn, ms, ...rest);
|
|
245
|
+
const unref = t.unref.bind(t);
|
|
246
|
+
const rec = { ms, unrefd: false };
|
|
247
|
+
t.unref = () => { rec.unrefd = true; return unref(); };
|
|
248
|
+
armed.push({ rec, t });
|
|
249
|
+
return t;
|
|
250
|
+
};
|
|
251
|
+
const key = require.resolve('../src/bongos/metrics.js');
|
|
252
|
+
const cached = require.cache[key];
|
|
253
|
+
try {
|
|
254
|
+
delete require.cache[key];
|
|
255
|
+
const fresh = require('../src/bongos/metrics.js');
|
|
256
|
+
assert.ok(armed.length >= 1, 'the lag sampler arms an interval');
|
|
257
|
+
for (const { rec } of armed) {
|
|
258
|
+
assert.ok(rec.ms >= 5000 && rec.ms <= 10_000, `interval of ${rec.ms} ms is outside 5-10 s`);
|
|
259
|
+
assert.ok(rec.unrefd, 'the sampler interval is unref\'d');
|
|
260
|
+
}
|
|
261
|
+
const out = await fresh.render({ async query() { return { rows: [] }; } });
|
|
262
|
+
assert.match(out, /# TYPE gds_nodejs_eventloop_lag_seconds gauge/);
|
|
263
|
+
assert.match(out, /gds_nodejs_eventloop_lag_seconds\{?\}? ?0/);
|
|
264
|
+
} finally {
|
|
265
|
+
globalThis.setInterval = realSetInterval;
|
|
266
|
+
for (const { t } of armed) clearInterval(t);
|
|
267
|
+
require.cache[key] = cached;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
147
271
|
summary();
|
|
@@ -279,6 +279,20 @@ t('numberCollisions: the one pre-existing collision is grandfathered', () => {
|
|
|
279
279
|
assert.equal(ns.numberCollisions(other).length, 1, '013 in the same module still fails');
|
|
280
280
|
});
|
|
281
281
|
|
|
282
|
+
t('numberCollisions: the core 283 pair is grandfathered, and 283 stays closed (task 1004954)', () => {
|
|
283
|
+
// Two renames off 282 both landed on 283 and are applied on the hub; recording them is
|
|
284
|
+
// the fix, because a third rename would re-run an applied migration.
|
|
285
|
+
const pair = ['claimable_ignores_page_markers', 'payout_approvals'].map((s) => ({
|
|
286
|
+
source: 'core', basename: `core_283_${s}.sql`,
|
|
287
|
+
}));
|
|
288
|
+
assert.deepEqual(ns.numberCollisions(pair), [], 'the landed 283 pair is exempt');
|
|
289
|
+
|
|
290
|
+
const three = [...pair, { source: 'core', basename: 'core_283_new.sql' }];
|
|
291
|
+
const c3 = ns.numberCollisions(three);
|
|
292
|
+
assert.equal(c3.length, 1, 'a NEW third file at 283 still fails');
|
|
293
|
+
assert.ok(c3[0].files.includes('core_283_new.sql'), 'and the new file is named in the violation');
|
|
294
|
+
});
|
|
295
|
+
|
|
282
296
|
t('numberCollisions: non-migration filenames are ignored', () => {
|
|
283
297
|
const files = [
|
|
284
298
|
{ source: 'module', moduleKey: 'economy', basename: 'README.md' },
|
package/tests/module_api.mjs
CHANGED
|
@@ -30,8 +30,10 @@ const PUBLISHED_SURFACE = [
|
|
|
30
30
|
// published because the artist gate's one escape hatch is the owner's alone, and
|
|
31
31
|
// a module cannot enforce that without being able to ask who the owner is.
|
|
32
32
|
'getFoundingBuilder',
|
|
33
|
+
'taskNeeds', // task 1004950 (ADR 0395, 1.24.0): what a task needs and what a builder can do — the feed and claim gate read it, and the session optimizer (core) applies the same rule
|
|
33
34
|
'sleep', // task 1004893: THE server-side "wait N milliseconds" (1.23.0) — modules wait through it, never a private copy
|
|
34
35
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
36
|
+
'prelaunchActive', // task 1004998 (1.24.0): is pre-launch (stealth) on? — the adopt preflight refuses a PUBLIC repo while it is
|
|
35
37
|
'grantedScopeCoversRepos', // the create-route credential preflight's scope read (task 1002909); kernel (auth-config.js), unlike db/github-repos which the module reaches via `pool` + its own client
|
|
36
38
|
'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
|
|
37
39
|
'publicProjectFeedRateLimit', // task 1002327 (ADR 0252 §5.3): the per-IP ceiling on GET /projects/featured — the platform's most expensive anonymous read (a cold call fans out to up to 500 third-party origins). One instance for the same reason as its neighbour
|
package/tests/module_loader.mjs
CHANGED
|
@@ -278,7 +278,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
278
278
|
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
279
279
|
loader._reset();
|
|
280
280
|
const snap = loader.loaderSnapshot();
|
|
281
|
-
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'design-styles', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'npm-release', 'onboarding', 'page-notes', 'pixel-art', 'platform-identity', 'provisioning', 'public-landing', 'raise', 'render-deploy', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
|
|
281
|
+
assert.deepEqual(snap.loaded, ['agents', 'ai-studio', 'autonomy', 'builder-settings', 'copy-desk', 'design-styles', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'market-watch', 'memory', 'npm-release', 'onboarding', 'page-notes', 'pixel-art', 'platform-identity', 'provisioning', 'public-landing', 'raise', 'render-deploy', 'security', 'sessions', 'specialities', 'status-ui', 'ui-design']);
|
|
282
282
|
assert.deepEqual(snap.errors, []);
|
|
283
283
|
assert.deepEqual(snap.skipped, []);
|
|
284
284
|
loader._reset();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// tests/module_skills_hook.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004915 — session start puts module skills in place for an artist-craft builder.
|
|
4
|
+
// Pins the pure decision and the printed line; the network leg is a thin raw fetch (as in
|
|
5
|
+
// recall_hints.mjs) and the copy itself is pinned by claude_materialize.mjs.
|
|
6
|
+
//
|
|
7
|
+
// Run: node tests/module_skills_hook.mjs
|
|
8
|
+
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { requireHostModule } from './helpers.mjs';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { wantsModuleSkills, missingModuleSkills, newlyLanded, formatLanded, DEADLINE_MS } =
|
|
15
|
+
requireHostModule(require, '../.claude/hooks/module-skills.js');
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
function t(name, fn) {
|
|
20
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
21
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
t('an artist-craft builder gets the skills', () => {
|
|
25
|
+
assert.equal(wantsModuleSkills({ builder: { preferred_disciplines: ['artist'] } }), true);
|
|
26
|
+
assert.equal(wantsModuleSkills({ builder: { preferred_disciplines: ['engineer', 'artist'] } }), true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
t('anyone else gets nothing (the 2026-09-02 rule holds)', () => {
|
|
30
|
+
assert.equal(wantsModuleSkills({ builder: { preferred_disciplines: ['engineer'] } }), false);
|
|
31
|
+
assert.equal(wantsModuleSkills({ builder: { preferred_disciplines: [] } }), false);
|
|
32
|
+
assert.equal(wantsModuleSkills({ builder: {} }), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
t('a missing or malformed lookup copies nothing', () => {
|
|
36
|
+
for (const bad of [null, undefined, {}, { builder: null }, { builder: { preferred_disciplines: 'artist' } }]) {
|
|
37
|
+
assert.equal(wantsModuleSkills(bad), false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
t('the line names each new skill and says when it shows up; silent when none landed', () => {
|
|
42
|
+
const line = formatLanded(['tweak']);
|
|
43
|
+
assert.ok(line.includes('/tweak') && line.includes('next session'));
|
|
44
|
+
assert.equal(formatLanded([]), null);
|
|
45
|
+
assert.equal(formatLanded(undefined), null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
t('only an enabled module absent skills are missing; with all in place nothing is (steady state: no lookup, no copy)', () => {
|
|
49
|
+
const roster = [
|
|
50
|
+
{ key: 'copy-desk', enabled: true, skills: ['tweak'] },
|
|
51
|
+
{ key: 'pixel-art', enabled: false, skills: ['otb-tile-generate'] },
|
|
52
|
+
];
|
|
53
|
+
assert.deepEqual(missingModuleSkills(roster, []), ['tweak']);
|
|
54
|
+
assert.deepEqual(missingModuleSkills(roster, ['tweak', 'design']), []);
|
|
55
|
+
assert.deepEqual(missingModuleSkills(undefined, undefined), []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('only what was missing AND landed is announced', () => {
|
|
59
|
+
assert.deepEqual(newlyLanded(['tweak'], ['tweak', 'other']), ['tweak']);
|
|
60
|
+
assert.deepEqual(newlyLanded(['tweak'], []), []);
|
|
61
|
+
assert.deepEqual(newlyLanded([], ['tweak']), []);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('the network budget stays under the session-start ceiling', () => {
|
|
65
|
+
assert.ok(DEADLINE_MS <= 2000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
69
|
+
process.exit(failed ? 1 : 0);
|
package/tests/notes.mjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// tests/notes.mjs — notes on a task or goal, grouped by purpose (task 1004745,
|
|
2
|
+
// goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// What is pinned: a note is sorted into a purpose (the author's choice, else a
|
|
5
|
+
// plain-word guess); the page gets the notes GROUPED by purpose in a fixed reading
|
|
6
|
+
// order with the newest few open and the rest folded; a note is about a task or a
|
|
7
|
+
// goal and never both or neither; and the purposes the code knows are exactly the
|
|
8
|
+
// ones the table's CHECK allows.
|
|
9
|
+
import { strict as assert } from 'node:assert';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const notes = require(path.join(ROOT, 'modules', 'lifecycle', 'notes.js'));
|
|
19
|
+
|
|
20
|
+
const at = (n) => new Date(Date.UTC(2026, 9, 1, 0, n)).toISOString();
|
|
21
|
+
const row = (id, kind, minute) => ({ id, kind, created_at: at(minute), body: `note ${id}` });
|
|
22
|
+
|
|
23
|
+
test('a note with no purpose is sorted by its words, and says it was guessed', () => {
|
|
24
|
+
const cases = [
|
|
25
|
+
['We decided to keep the old route.', 'decision'],
|
|
26
|
+
['Risk: this could break the claim gate.', 'risk'],
|
|
27
|
+
['Should the page show drafts?', 'question'],
|
|
28
|
+
['See https://example.com/spec for the layout', 'reference'],
|
|
29
|
+
['Started on the migration, the route is next step', 'progress'],
|
|
30
|
+
['Out of scope: the mobile layout', 'scope'],
|
|
31
|
+
['hello there', 'other'],
|
|
32
|
+
];
|
|
33
|
+
for (const [text, kind] of cases) {
|
|
34
|
+
assert.equal(notes.suggestKind(text), kind, text);
|
|
35
|
+
const v = notes.validateNote({ body: text, task_id: 5 });
|
|
36
|
+
assert.equal(v.ok, true);
|
|
37
|
+
assert.equal(v.kind, kind);
|
|
38
|
+
assert.equal(v.kindSource, 'auto');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a purpose the author chose is kept and recorded as the author\'s', () => {
|
|
43
|
+
const v = notes.validateNote({ body: 'Should we?', kind: 'scope', goal_id: 2 });
|
|
44
|
+
assert.equal(v.ok, true);
|
|
45
|
+
assert.equal(v.kind, 'scope');
|
|
46
|
+
assert.equal(v.kindSource, 'author');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a note is refused with no body, an unknown purpose, or the wrong subject', () => {
|
|
50
|
+
const reasons = (i) => notes.validateNote(i).errors.map((e) => `${e.field}:${e.reason}`);
|
|
51
|
+
assert.ok(reasons({ body: ' ', task_id: 1 }).includes('body:required'));
|
|
52
|
+
assert.ok(reasons({ body: 'x'.repeat(notes.MAX_BODY + 1), task_id: 1 }).some((r) => r.startsWith('body:at most')));
|
|
53
|
+
assert.ok(reasons({ body: 'x', kind: 'gossip', task_id: 1 }).some((r) => r.startsWith('kind:')));
|
|
54
|
+
assert.ok(reasons({ body: 'x' }).some((r) => r.startsWith('subject:')), 'neither subject');
|
|
55
|
+
assert.ok(reasons({ body: 'x', task_id: 1, goal_id: 2 }).some((r) => r.startsWith('subject:')), 'both subjects');
|
|
56
|
+
assert.ok(reasons({ body: 'x', kind: 'gossip', task_id: 1 }).join().includes('Open questions'), 'the refusal names groups by their labels');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('notes group by purpose in reading order, empty groups left out', () => {
|
|
60
|
+
const groups = notes.groupNotes([
|
|
61
|
+
row(1, 'reference', 1), row(2, 'question', 2), row(3, 'scope', 3), row(4, 'question', 4),
|
|
62
|
+
]);
|
|
63
|
+
assert.deepEqual(groups.map((g) => g.kind), ['scope', 'question', 'reference']);
|
|
64
|
+
assert.deepEqual(groups.find((g) => g.kind === 'question').count, 2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('inside a group the newest comes first, the newest few are open and the rest are folded', () => {
|
|
68
|
+
const rows = [1, 2, 3, 4, 5].map((i) => row(i, 'decision', i));
|
|
69
|
+
const [g] = notes.groupNotes(rows);
|
|
70
|
+
assert.deepEqual(g.open.map((n) => n.id), [5, 4, 3]);
|
|
71
|
+
assert.deepEqual(g.folded.map((n) => n.id), [2, 1]);
|
|
72
|
+
assert.equal(g.count, 5);
|
|
73
|
+
assert.equal(g.latest_at, at(5));
|
|
74
|
+
assert.equal(notes.groupNotes(rows, { openPerGroup: 1 })[0].folded.length, 4);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a note under an unknown purpose shows under Other rather than vanishing', () => {
|
|
78
|
+
const groups = notes.groupNotes([row(1, 'mystery', 1)]);
|
|
79
|
+
assert.deepEqual(groups.map((g) => g.kind), ['other']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the purposes in code are exactly the ones the table allows', () => {
|
|
83
|
+
const sql = fs.readFileSync(path.join(ROOT, 'modules', 'lifecycle', 'migrations', 'lifecycle_017_notes.sql'), 'utf8');
|
|
84
|
+
const m = sql.match(/lifecycle_notes_kind_chk\s+CHECK \(kind IN \(([^)]*)\)\)/);
|
|
85
|
+
assert.ok(m, 'the kind CHECK is present');
|
|
86
|
+
const allowed = m[1].split(',').map((s) => s.trim().replace(/'/g, '')).sort();
|
|
87
|
+
assert.deepEqual(allowed, [...notes.KIND_KEYS].sort());
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// A fake pool that answers by the SQL it is handed, so the data layer runs for real.
|
|
91
|
+
function fakePool() {
|
|
92
|
+
const calls = [];
|
|
93
|
+
const store = [];
|
|
94
|
+
return {
|
|
95
|
+
calls,
|
|
96
|
+
query: async (sql, params) => {
|
|
97
|
+
calls.push({ sql, params });
|
|
98
|
+
if (/INSERT INTO lifecycle_notes/.test(sql)) {
|
|
99
|
+
const [task_id, goal_id, author_id, body, kind, kind_source] = params;
|
|
100
|
+
const r = { id: String(store.length + 1), task_id, goal_id, author_id, body, kind, kind_source, created_at: at(store.length + 1), updated_at: at(store.length + 1) };
|
|
101
|
+
store.push(r);
|
|
102
|
+
return { rows: [{ ...r, author_login: 'sam', author_name: 'Sam', author_avatar_url: null }], rowCount: 1 };
|
|
103
|
+
}
|
|
104
|
+
if (/count\(\*\)/.test(sql)) {
|
|
105
|
+
const col = /task_id = \$1/.test(sql) ? 'task_id' : 'goal_id';
|
|
106
|
+
return { rows: [{ n: store.filter((x) => String(x[col]) === String(params[0])).length }] };
|
|
107
|
+
}
|
|
108
|
+
if (/FROM tasks t LEFT JOIN goals g/.test(sql)) {
|
|
109
|
+
if (params[0] === 6) return { rows: [] };
|
|
110
|
+
return { rows: params[0] === 7 ? [{ goal_id: 40, visibility: 'private' }] : [{ goal_id: null, visibility: null }] };
|
|
111
|
+
}
|
|
112
|
+
if (/FROM goals g WHERE g\.id/.test(sql)) return { rows: params[0] === 99 ? [] : [{ goal_id: params[0], visibility: params[0] === 40 ? 'private' : 'public' }] };
|
|
113
|
+
if (/FROM goal_members/.test(sql)) return { rows: params[1] === 11 ? [{}] : [] };
|
|
114
|
+
if (/FROM lifecycle_notes n JOIN builders/.test(sql)) {
|
|
115
|
+
const col = /n\.task_id = \$1/.test(sql) ? 'task_id' : 'goal_id';
|
|
116
|
+
return { rows: store.filter((x) => String(x[col]) === String(params[0])).reverse() };
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`unexpected SQL: ${sql}`);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
test('adding and reading notes goes through the real data layer, task and goal kept apart', async () => {
|
|
124
|
+
const pool = fakePool();
|
|
125
|
+
const a = await notes.addNote({ taskId: 5, authorId: 9, body: 'Decided: ship behind a flag', kind: 'decision', kindSource: 'author' }, { pool });
|
|
126
|
+
await notes.addNote({ goalId: 2, authorId: 9, body: 'What is the goal?', kind: 'question', kindSource: 'author' }, { pool });
|
|
127
|
+
assert.equal(a.author_name, 'Sam');
|
|
128
|
+
assert.deepEqual((await notes.listNotes({ taskId: 5 }, { pool })).map((n) => n.body), ['Decided: ship behind a flag']);
|
|
129
|
+
assert.deepEqual((await notes.listNotes({ goalId: 2 }, { pool })).map((n) => n.body), ['What is the goal?']);
|
|
130
|
+
const someone = { id: 12, rank: 'thetes' };
|
|
131
|
+
assert.equal(await notes.canSee(someone, { taskId: 5 }, { pool }), true);
|
|
132
|
+
assert.equal(await notes.canSee(someone, { taskId: 6 }, { pool }), false, 'a missing task reads as not found');
|
|
133
|
+
assert.equal(await notes.canSee({ id: 13, rank: 'archon' }, { goalId: 99 }, { pool }), false, 'even an Archon gets not found for a missing goal');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('exactly one subject is accepted, as a positive whole number', () => {
|
|
137
|
+
assert.deepEqual(notes.subjectOf({ task_id: '5' }), { taskId: 5 });
|
|
138
|
+
assert.deepEqual(notes.subjectOf({ goal_id: 2 }), { goalId: 2 });
|
|
139
|
+
assert.equal(notes.subjectOf({}), null);
|
|
140
|
+
assert.equal(notes.subjectOf({ task_id: 1, goal_id: 2 }), null);
|
|
141
|
+
assert.equal(notes.subjectOf({ task_id: 'abc' }), null);
|
|
142
|
+
assert.equal(notes.subjectOf({ task_id: -3 }), null);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('a private goal\'s notes are for its members and an Archon, and so are its tasks\' notes', async () => {
|
|
146
|
+
const pool = fakePool();
|
|
147
|
+
const member = { id: 11, rank: 'thetes' };
|
|
148
|
+
const outsider = { id: 12, rank: 'metic' };
|
|
149
|
+
const archon = { id: 13, rank: 'archon' };
|
|
150
|
+
assert.equal(await notes.canSee(member, { goalId: 40 }, { pool }), true);
|
|
151
|
+
assert.equal(await notes.canSee(outsider, { goalId: 40 }, { pool }), false, 'a Metic who is not a member');
|
|
152
|
+
assert.equal(await notes.canSee(archon, { goalId: 40 }, { pool }), true);
|
|
153
|
+
assert.equal(await notes.canSee(outsider, { taskId: 7 }, { pool }), false, 'a task inside the private goal');
|
|
154
|
+
assert.equal(await notes.canSee(member, { taskId: 7 }, { pool }), true);
|
|
155
|
+
assert.equal(await notes.canSee(outsider, { goalId: 2 }, { pool }), true, 'a public goal is open');
|
|
156
|
+
assert.equal(await notes.canSee(outsider, { taskId: 5 }, { pool }), true, 'a task with no goal is open');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('only the author or a Metic and above may re-file or delete a note', () => {
|
|
160
|
+
const note = { author_id: '9' };
|
|
161
|
+
assert.equal(notes.canChange({ id: 9, rank: 'xenos' }, note), true, 'the author');
|
|
162
|
+
assert.equal(notes.canChange({ id: 10, rank: 'thetes' }, note), false);
|
|
163
|
+
assert.equal(notes.canChange({ id: 10, rank: 'metic' }, note), true);
|
|
164
|
+
assert.equal(notes.canChange({ id: 10, rank: 'archon' }, note), true);
|
|
165
|
+
assert.equal(notes.canChange({ id: 10, rank: 'nonsense' }, note), false, 'an unknown rank fails closed');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('a task or goal holds a bounded number of notes', async () => {
|
|
169
|
+
const pool = fakePool();
|
|
170
|
+
for (let i = 0; i < 3; i += 1) await notes.addNote({ taskId: 5, authorId: 9, body: 'n', kind: 'other', kindSource: 'auto' }, { pool });
|
|
171
|
+
assert.equal(await notes.countNotes({ taskId: 5 }, { pool }), 3);
|
|
172
|
+
assert.equal(await notes.countNotes({ goalId: 2 }, { pool }), 0);
|
|
173
|
+
assert.ok(notes.MAX_NOTES_PER_SUBJECT > 3);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('the route is registered, and its author is the caller and never the body', () => {
|
|
177
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'notes.js'), 'utf8');
|
|
178
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'lifecycle', 'module.json'), 'utf8'));
|
|
179
|
+
assert.ok(manifest.contributes.routes.includes('notes'));
|
|
180
|
+
assert.ok(/authorId: req\.builder\.id/.test(route), 'author is the caller, never the body');
|
|
181
|
+
assert.ok(!/authorId: (body|req\.body)/.test(route));
|
|
182
|
+
});
|
|
@@ -89,6 +89,11 @@ const OWN_SCOPED_SURFACES = [
|
|
|
89
89
|
why: "the caller's OWN hardware security cards — list, register, rename, remove (task 1004746, ADR 0382). Every store call is keyed on req.builder.id; a credential id belonging to somebody else matches no row. The one ungated route serves a vendored static library file.",
|
|
90
90
|
crossOwner: [],
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
file: 'src/bongos/routes/my-payouts.js',
|
|
94
|
+
why: "the CALLER's own side of the payout gates (task 1004914, ADR 0385 D8): their payout standing and holds, declaring their own age tier, appealing a hold on their own pay (anyone else's answers 404, and the database refuses an appeal by anyone but the frozen person), and co-signing as themselves where the database checks they are the current guardian. Every route acts on req.builder.id. The approver's routes live apart in routes/payout-gates.js for exactly that reason.",
|
|
95
|
+
crossOwner: [],
|
|
96
|
+
},
|
|
92
97
|
{
|
|
93
98
|
file: 'modules/specialities/routes/specialities.js',
|
|
94
99
|
why: "role sub-specialities (task 1003855, ADR 0296). The own-scoped route here is GET /me/specialities — the CALLER's own adoptions, taken from req.builder.id with nothing read off the request. The rest of the router is /specialities/* rather than /me/*, and those are ownership-checked per row against req.builder.id after re-reading the speciality server-side, never from a client-supplied owner id.",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tests/page_tweak_marker_sql.mjs — the page-marker predicate and every copy of it
|
|
2
|
+
// (task 1004947). DB-free: the real-Postgres twin is page_tweak_no_block.mjs.
|
|
3
|
+
//
|
|
4
|
+
// An artist's page claim is a marker, so each overlap read that guards engineering
|
|
5
|
+
// work leaves it out. This drives the real feed and the real batch dry-run through
|
|
6
|
+
// a recording pool and checks the SQL each one SENDS carries the predicate, then
|
|
7
|
+
// pins the three hand copies (the helper, the optimizer's inlined guard, the
|
|
8
|
+
// claimable_tasks view migration) to one another, the way claimable_blocker_status
|
|
9
|
+
// pins the blocker and dependency guards.
|
|
10
|
+
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { makeRunner } from './helpers.mjs';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { test, summary } = makeRunner();
|
|
17
|
+
|
|
18
|
+
const flat = (s) => String(s).replace(/\s+/g, ' ').trim();
|
|
19
|
+
const read = (...p) => readFileSync(new URL(['..', ...p].join('/'), import.meta.url), 'utf8');
|
|
20
|
+
const { notPageMarkerSql } = require('../modules/lifecycle/page-tweak-artist.js');
|
|
21
|
+
|
|
22
|
+
const MARKER = "NOT (COALESCE(t.source, '') = 'page-tweak' AND c.worktree_name IS NULL AND c.creator_session_id IS NULL)";
|
|
23
|
+
const MARKER_T2 = MARKER.replace('t.source', 't2.source');
|
|
24
|
+
|
|
25
|
+
await test('the predicate names the page-tweak source and both NULLs of a web claim', () => {
|
|
26
|
+
assert.equal(flat(notPageMarkerSql('c', 't')), MARKER);
|
|
27
|
+
assert.equal(flat(notPageMarkerSql('cl', 'tk')),
|
|
28
|
+
"NOT (COALESCE(tk.source, '') = 'page-tweak' AND cl.worktree_name IS NULL AND cl.creator_session_id IS NULL)");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await test('a NULL task source keeps the claim COUNTED (the predicate cannot go NULL)', () => {
|
|
32
|
+
// SQL three-valued logic: `NULL = 'page-tweak'` is NULL, and NOT (NULL AND true AND true)
|
|
33
|
+
// is NULL, which WHERE reads as false — the claim would silently vanish. COALESCE is
|
|
34
|
+
// what stops that, so a source-less task's both-NULL claim still counts.
|
|
35
|
+
assert.match(flat(notPageMarkerSql('c', 't')), /COALESCE\(t\.source, ''\) = 'page-tweak'/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await test('the optimizer hand copy and the view migration carry the same predicate', () => {
|
|
39
|
+
const opt = flat(read('src', 'bongos', 'optimizer.js'));
|
|
40
|
+
assert.ok(opt.includes(`const PAGE_MARKER_GUARD = \`${MARKER_T2}\`;`), 'optimizer declares the guard once, verbatim');
|
|
41
|
+
assert.ok(opt.includes('AND ${PAGE_MARKER_GUARD} AND t2.touches && t.touches'), 'and the claimable query uses it before the overlap test');
|
|
42
|
+
const mig = flat(read('migrations', 'core_283_claimable_ignores_page_markers.sql').replace(/^\s*--.*$/gm, ''));
|
|
43
|
+
assert.ok(mig.includes(`${MARKER_T2} AND t2.touches && t.touches`), 'the view migration carries it verbatim');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Route the module pool through a recorder, then drive the real functions.
|
|
47
|
+
const api = require('../src/module-api.js');
|
|
48
|
+
const db = require('../modules/lifecycle/db.js');
|
|
49
|
+
const sent = [];
|
|
50
|
+
async function recording(fn) {
|
|
51
|
+
const orig = api.pool.query;
|
|
52
|
+
api.pool.query = async (sql, params) => {
|
|
53
|
+
sent.push(flat(sql));
|
|
54
|
+
if (/FROM tasks/.test(flat(sql)) && /ANY\(\$1/.test(flat(sql)) && !/FROM claims/.test(flat(sql))) {
|
|
55
|
+
return { rows: [{ id: params[0][0], status: 'ready', touches: ['a'], module_key: null, requires_rank: 'xenos', version_id: 'V' }] };
|
|
56
|
+
}
|
|
57
|
+
return { rows: [] };
|
|
58
|
+
};
|
|
59
|
+
try { sent.length = 0; await fn(); } finally { api.pool.query = orig; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await test('the batch dry-run overlap read leaves page markers out', async () => {
|
|
63
|
+
await recording(() => db.validateClaimBatch({ taskIds: ['1'], builderId: '1' }).catch(() => {}));
|
|
64
|
+
const overlap = sent.find((s) => /FROM claims c JOIN tasks t/.test(s) && /released_at IS NULL/.test(s) && /t\.touches/.test(s));
|
|
65
|
+
assert.ok(overlap, 'the dry-run sent its active-claims read');
|
|
66
|
+
assert.ok(overlap.includes(MARKER), 'and it carries the marker predicate');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await test('the claimable feed overlap clause leaves page markers out', async () => {
|
|
70
|
+
await recording(() => db.listClaimableTasks({}));
|
|
71
|
+
const feed = sent.find((s) => /t2\.touches && t\.touches/.test(s));
|
|
72
|
+
assert.ok(feed, 'the feed sent its overlap clause');
|
|
73
|
+
assert.ok(feed.includes(MARKER_T2), 'and it carries the marker predicate');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
summary();
|