@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
|
@@ -487,14 +487,19 @@ async function reorderGoals({ versionId, orderedIds }, deps = {}) {
|
|
|
487
487
|
// FOR UPDATE on its transaction client, and this UPDATE on a pool connection
|
|
488
488
|
// blocks on that lock — while the transaction is awaiting this UPDATE before it
|
|
489
489
|
// can COMMIT and release it. Neither side can move.
|
|
490
|
-
|
|
490
|
+
//
|
|
491
|
+
// `fromStatus` (task 1001693) makes the move conditional: the row flips only if it
|
|
492
|
+
// still reads that status, and null comes back otherwise. The achieve path uses it
|
|
493
|
+
// so exactly one caller wins a goal's flip to `achieved`, and only the winner books
|
|
494
|
+
// the goal-achievement karma.
|
|
495
|
+
async function setGoalStatus({ goalId, status, fromStatus = null }, deps = {}) {
|
|
491
496
|
const activePool = executorFor(deps, pool);
|
|
492
497
|
const { rows } = await activePool.query(
|
|
493
498
|
`UPDATE goals
|
|
494
499
|
SET status = $2, updated_at = now()
|
|
495
|
-
WHERE id = $1
|
|
500
|
+
WHERE id = $1 AND ($3::text IS NULL OR status = $3::text)
|
|
496
501
|
RETURNING ${GOAL_COLS}`,
|
|
497
|
-
[goalId, status]
|
|
502
|
+
[goalId, status, fromStatus]
|
|
498
503
|
);
|
|
499
504
|
// A goal leaving `open` takes no new members, so its pending asks are dead.
|
|
500
505
|
if (rows[0] && status !== 'open') await cancelPendingMembershipRequests(activePool, goalId);
|
|
@@ -547,7 +552,16 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
|
|
|
547
552
|
// This never blocks a SHIP; it declines only the goal's flip to achieved, and
|
|
548
553
|
// the goal simply stays open until its last task lands or is abandoned.
|
|
549
554
|
if (unfinishedTasks > 0) return { achieved: false, goal, unfinishedTasks };
|
|
550
|
-
|
|
555
|
+
// Conditional on still reading `open` (task 1001693): two overrides closing a
|
|
556
|
+
// goal's last criteria at once both reach here, and only one may flip it and
|
|
557
|
+
// book its karma. The loser reports the goal as achieved, which it now is, and
|
|
558
|
+
// leaves the reward and the version close to the winner.
|
|
559
|
+
const updated = await setGoalStatus({ goalId, status: 'achieved', fromStatus: 'open' }, deps);
|
|
560
|
+
if (!updated) return { achieved: true, goal: await getGoal(goalId, { ...deps, forUpdate: false }), versionsClosed: [] };
|
|
561
|
+
const exec = executorFor(deps, pool);
|
|
562
|
+
// The same reward the ship-time flip books, through the same helper, so a failed
|
|
563
|
+
// booking is logged the same way on both doors.
|
|
564
|
+
await doneWhen.rewardAchievedGoals(exec, [updated]);
|
|
551
565
|
|
|
552
566
|
// BV1.R16 (task 1003603, ADR 0263 §§3–4): the SECOND achievement writer. A goal
|
|
553
567
|
// reaching `achieved` may have been the version's last non-maintenance goal, and
|
|
@@ -559,7 +573,6 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
|
|
|
559
573
|
//
|
|
560
574
|
// Runs on the caller's exec so it sees the flip above, which in the ship path is
|
|
561
575
|
// still uncommitted. Never throws: autoCloseVersionsForGoals catches per version.
|
|
562
|
-
const exec = executorFor(deps, pool);
|
|
563
576
|
const versionsClosed = await autoCloseVersionsForGoals(exec, [updated]);
|
|
564
577
|
return { achieved: true, goal: updated, versionsClosed };
|
|
565
578
|
}
|
|
@@ -22,6 +22,8 @@ const { isReservedCriterionSlug } = require('./criterion-ref.js');
|
|
|
22
22
|
const { resolveWriteDiscipline, WRITABLE_DISCIPLINES } = require('./task-classifier.js');
|
|
23
23
|
const { gateOnSpecTask } = require('./goal-spec-task.js');
|
|
24
24
|
const { ensureArtistReviewGoal } = require('./artist-review-goal.js');
|
|
25
|
+
const { notPageMarkerSql } = require('./page-tweak-artist.js');
|
|
26
|
+
const { needsFilterWhere, capabilityFeedWhere } = require('./task-needs-gate.js');
|
|
25
27
|
|
|
26
28
|
// -------------------------------------------------------------------------
|
|
27
29
|
// Tasks
|
|
@@ -147,6 +149,7 @@ async function createTask({
|
|
|
147
149
|
// (requiresRank stays null for the goal vector → derived purely from touches +
|
|
148
150
|
// sensitivity), so goal_id never influences the requires_rank clamp.
|
|
149
151
|
goalId = null,
|
|
152
|
+
needs = [], // task 1004950 / ADR 0395 — ⊆ ['owner','server']; routes normalize, the CHECK backstops
|
|
150
153
|
// task 1003278: WHO created it — the builder id from the authenticated request.
|
|
151
154
|
// Nullable, appended at the end (the goals bind-order convention): a system
|
|
152
155
|
// path with no builder in the request (a seed, a migration backfill) passes
|
|
@@ -267,13 +270,13 @@ async function createTask({
|
|
|
267
270
|
est_minutes, est_cost_usd, manual_degree, priority,
|
|
268
271
|
automation_tag, credits_reward, source, source_ref, value_summary,
|
|
269
272
|
kind, discipline, parallel_safe, newcomer_friendly, needs_migration,
|
|
270
|
-
security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at)
|
|
273
|
+
security_sensitive, requires_rank, module_key, goal_id, created_by, promoted_at, needs)
|
|
271
274
|
VALUES ($1, $2, $3, $4, $5,
|
|
272
275
|
$6, $7, $8, $9,
|
|
273
276
|
$10, $11, $12, $13, $14,
|
|
274
277
|
$15, $16, $17, $18, $19,
|
|
275
278
|
$20, $21, $22, $23, $24,
|
|
276
|
-
CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END)
|
|
279
|
+
CASE WHEN $4 IN ('ready','active') THEN now() ELSE NULL END, $25)
|
|
277
280
|
RETURNING *`,
|
|
278
281
|
[
|
|
279
282
|
versionId,
|
|
@@ -300,6 +303,7 @@ async function createTask({
|
|
|
300
303
|
moduleKey ?? null,
|
|
301
304
|
goalIdEffective ?? null,
|
|
302
305
|
createdBy ?? null,
|
|
306
|
+
Array.isArray(needs) ? needs : [],
|
|
303
307
|
]
|
|
304
308
|
);
|
|
305
309
|
// task 1677 / ADR 0096: audit each creation-time auto-assignment so the trail is
|
|
@@ -357,7 +361,7 @@ async function findTaskBySourceRef({ source, sourceRef } = {}, deps = {}) {
|
|
|
357
361
|
|
|
358
362
|
const TASK_SETTER_COLUMNS = new Set([
|
|
359
363
|
'kind', 'discipline', 'newcomer_friendly', 'security_sensitive', 'parallel_safe', 'automation_tag',
|
|
360
|
-
'needs_migration', 'module_key', 'description', 'title', 'priority', 'value_summary',
|
|
364
|
+
'needs_migration', 'module_key', 'description', 'title', 'priority', 'value_summary', 'needs',
|
|
361
365
|
]);
|
|
362
366
|
|
|
363
367
|
async function updateTaskColumn(id, column, value) {
|
|
@@ -647,6 +651,7 @@ async function updateTaskParallelSafe(id, parallelSafe) { return updateTaskColum
|
|
|
647
651
|
// and leave the tag unset, then later judge that a task is safe to delegate.
|
|
648
652
|
|
|
649
653
|
async function updateTaskAutomationTag(id, automationTag) { return updateTaskColumn(id, 'automation_tag', automationTag); }
|
|
654
|
+
async function updateTaskNeeds(id, needs) { return updateTaskColumn(id, 'needs', needs); }
|
|
650
655
|
|
|
651
656
|
// task 1002648: correct the create-time migration heuristic's verdict — a false
|
|
652
657
|
// flag makes claiming reserve a migration number that goes unused (task 1002614
|
|
@@ -777,7 +782,7 @@ async function getTask(id) {
|
|
|
777
782
|
}
|
|
778
783
|
|
|
779
784
|
|
|
780
|
-
async function listTasks({ versionId, status, kind, discipline, goalId, createdBy, staleDays, limit, offset } = {}) {
|
|
785
|
+
async function listTasks({ versionId, status, kind, discipline, goalId, createdBy, staleDays, limit, offset, needs } = {}) {
|
|
781
786
|
const params = [];
|
|
782
787
|
const where = [];
|
|
783
788
|
if (versionId) {
|
|
@@ -800,6 +805,7 @@ async function listTasks({ versionId, status, kind, discipline, goalId, createdB
|
|
|
800
805
|
params.push(goalId);
|
|
801
806
|
where.push(`goal_id = $${params.length}`);
|
|
802
807
|
}
|
|
808
|
+
const needsWhere = needsFilterWhere(needs, '', params); if (needsWhere) where.push(needsWhere); // task 1004950
|
|
803
809
|
// task 1003278 — the creator + staleness axes, server-side. They exist HERE and
|
|
804
810
|
// not only in the browser because the board was reducing the whole table to
|
|
805
811
|
// answer them: GET /tasks?status=backlog measured 690,625 bytes on 2026-08-25,
|
|
@@ -1015,6 +1021,7 @@ const CLAIMABLE_SQL = `
|
|
|
1015
1021
|
SELECT 1 FROM claims c
|
|
1016
1022
|
JOIN tasks t2 ON t2.id = c.task_id
|
|
1017
1023
|
WHERE c.released_at IS NULL
|
|
1024
|
+
AND ${notPageMarkerSql('c', 't2')}
|
|
1018
1025
|
AND t2.touches && t.touches
|
|
1019
1026
|
)`;
|
|
1020
1027
|
const SESSION_FIT_SQL = `
|
|
@@ -1028,7 +1035,7 @@ const SESSION_FIT_SQL = `
|
|
|
1028
1035
|
END AS fits_session_modes
|
|
1029
1036
|
FROM tasks t`;
|
|
1030
1037
|
|
|
1031
|
-
async function listClaimableTasks({ versionId = null, builderId = null, discipline = null, fit = null, createdBy = null, staleDays = null } = {}) {
|
|
1038
|
+
async function listClaimableTasks({ versionId = null, builderId = null, discipline = null, fit = null, createdBy = null, staleDays = null, needs = null } = {}) {
|
|
1032
1039
|
const params = [];
|
|
1033
1040
|
const where = [];
|
|
1034
1041
|
let goalJoin = '';
|
|
@@ -1037,6 +1044,7 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
|
|
|
1037
1044
|
params.push(versionId);
|
|
1038
1045
|
where.push(`v.version_id = $${params.length}`);
|
|
1039
1046
|
}
|
|
1047
|
+
const needsWhere = needsFilterWhere(needs, 'v', params); if (needsWhere) where.push(needsWhere); // task 1004950: the 'waiting on you' queue
|
|
1040
1048
|
if (discipline) {
|
|
1041
1049
|
params.push(discipline);
|
|
1042
1050
|
where.push(`v.discipline = $${params.length}`);
|
|
@@ -1110,6 +1118,7 @@ async function listClaimableTasks({ versionId = null, builderId = null, discipli
|
|
|
1110
1118
|
selectCols = `v.*, g.visibility AS g_visibility, g.scope_modules AS g_scope_modules, g.title AS g_title,
|
|
1111
1119
|
(gm.builder_id IS NOT NULL
|
|
1112
1120
|
OR (SELECT rank FROM builders WHERE id = $${params.length}) = 'archon') AS g_is_member`;
|
|
1121
|
+
where.push(await capabilityFeedWhere(builderId, fit, 'v', params)); // task 1004950 / ADR 0395: only what the viewer can do
|
|
1113
1122
|
}
|
|
1114
1123
|
// Autonomy / parallel-eligibility filter. Joining the session-fit relation
|
|
1115
1124
|
// (also a SELECT t.* shape) makes bare column names like priority/created_at
|
|
@@ -1470,7 +1479,7 @@ module.exports = {
|
|
|
1470
1479
|
suggestCreditsReward,
|
|
1471
1480
|
taskTouchesProtectedPath,
|
|
1472
1481
|
updateCriterionGoal,
|
|
1473
|
-
updateTaskAutomationTag,
|
|
1482
|
+
updateTaskAutomationTag, updateTaskNeeds,
|
|
1474
1483
|
updateTaskDescription,
|
|
1475
1484
|
updateTaskTitle,
|
|
1476
1485
|
updateTaskDiscipline,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// POST /api/bongos/done-when/:criterionId/satisfy — auth
|
|
24
24
|
// POST /api/bongos/done-when/:criterionId/unsatisfy — auth
|
|
25
25
|
|
|
26
|
-
const { pool } = require('../../src/module-api');
|
|
26
|
+
const { pool, resolveOptional } = require('../../src/module-api');
|
|
27
27
|
const { nonTerminalSql } = require('./task-status');
|
|
28
28
|
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
29
29
|
const { executorFor } = require('./db-executor.js');
|
|
@@ -618,9 +618,30 @@ async function achieveCompletedGoals(exec, goalIds) {
|
|
|
618
618
|
// Task 1003660: an achieved goal takes no new members — close its pending asks
|
|
619
619
|
// on the same executor, so the cancel rides the ship transaction.
|
|
620
620
|
await cancelPendingMembershipRequests(exec, rows.map((g) => g.id));
|
|
621
|
+
await rewardAchievedGoals(exec, rows);
|
|
621
622
|
return rows;
|
|
622
623
|
}
|
|
623
624
|
|
|
625
|
+
// rewardAchievedGoals — the goal-achievement karma (task 1001693 / BV1.P5.3, ADR
|
|
626
|
+
// 0101 Part 3), booked through the economy's `reward` port for each goal THIS
|
|
627
|
+
// statement flipped. Only the caller whose UPDATE ... WHERE status = 'open'
|
|
628
|
+
// returned the row gets here, so a concurrent ship or sweep cannot award twice.
|
|
629
|
+
// Optional and best-effort: no economy module means no karma, and the award runs
|
|
630
|
+
// under its own SAVEPOINT (goal-reward.js) so a failed karma write cannot abort
|
|
631
|
+
// the ship that achieved the goal. The catch here is for a provider that throws.
|
|
632
|
+
async function rewardAchievedGoals(exec, goals) {
|
|
633
|
+
const reward = resolveOptional('reward');
|
|
634
|
+
if (!reward || typeof reward.awardGoalAchievementKarma !== 'function') return;
|
|
635
|
+
for (const g of goals) {
|
|
636
|
+
try {
|
|
637
|
+
const r = await reward.awardGoalAchievementKarma(exec, { goalId: g.id });
|
|
638
|
+
if (r && r.reason === 'failed') console.warn(`[lifecycle] goal ${g.id} achieved; its karma was not booked: ${r.error}`);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
console.warn(`[lifecycle] goal ${g.id} achieved; its karma was not booked: ${e && e.message}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
624
645
|
// describeAutoClose — the one-line summary of what a close actually closed, or
|
|
625
646
|
// null on a no-op. Pure + here rather than in db.js because db.js is over the
|
|
626
647
|
// fitness oversize threshold: growth there has to be paid for, and formatting is
|
|
@@ -674,6 +695,7 @@ module.exports = {
|
|
|
674
695
|
autoSatisfyShippedCriteria,
|
|
675
696
|
describeAutoClose,
|
|
676
697
|
closeCompletedWorkForShip,
|
|
698
|
+
rewardAchievedGoals,
|
|
677
699
|
markCriterion,
|
|
678
700
|
unmarkCriterion,
|
|
679
701
|
};
|
|
@@ -26,6 +26,7 @@ const activity = require('./activity');
|
|
|
26
26
|
const mingle = require('./mingle');
|
|
27
27
|
const pageTweakReads = require('./page-tweak-reads');
|
|
28
28
|
const pageTweakClaim = require('./page-tweak-claim');
|
|
29
|
+
const pageNotice = require('./page-notice');
|
|
29
30
|
const pageTweakWrite = require('./page-tweak-write');
|
|
30
31
|
const pageTweakApprove = require('./page-tweak-approve');
|
|
31
32
|
|
|
@@ -165,6 +166,10 @@ module.exports = {
|
|
|
165
166
|
// read the part-3 claim gate uses, surfaced to me.js so the CLI + hall can
|
|
166
167
|
// show the owed-rebase warning that gates new claims (reward lands on ship).
|
|
167
168
|
needsRebaseTasksForBuilder: (...a) => db.needsRebaseTasksForBuilder(...a),
|
|
169
|
+
// task 1004948: the page-update heads-up lines for a claim's files, for GET /me.
|
|
170
|
+
noticesForClaims: (...a) => pageNotice.noticesForClaims(...a),
|
|
171
|
+
// task 1004948: engineering claims open on a page's files, for the artist's editor read.
|
|
172
|
+
engineeringActiveOnPage: (...a) => pageNotice.engineeringActiveOnPage(...a),
|
|
168
173
|
// builders.js (the Archon roster): the roster + stale-claim + per-builder grade.
|
|
169
174
|
getBuildersRoster: (...a) => db.getBuildersRoster(...a),
|
|
170
175
|
getActiveClaimsForBuilderWithStale: (...a) => db.getActiveClaimsForBuilderWithStale(...a),
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- lifecycle_017_notes.sql — notes on a task or a goal (task 1004745, goal 1000095).
|
|
2
|
+
--
|
|
3
|
+
-- WHAT IT IS. A place to put information that helps scope a task or goal, or helps
|
|
4
|
+
-- finish it: a decision, an open question, a link, a risk, progress. Each note is
|
|
5
|
+
-- short, dated and has an author.
|
|
6
|
+
--
|
|
7
|
+
-- WHY `kind` IS A COLUMN. Notes are shown GROUPED BY PURPOSE (scope, decision,
|
|
8
|
+
-- question, reference, progress, risk, other) instead of as one growing list, so the
|
|
9
|
+
-- purpose has to be stored, not re-guessed on every read. `kind_source` records
|
|
10
|
+
-- whether the author chose it or the heuristic did, so a wrong guess can be told
|
|
11
|
+
-- apart from a deliberate choice and a later smarter grouper knows which to revisit.
|
|
12
|
+
--
|
|
13
|
+
-- A note is about a task OR a goal, never both, and never neither.
|
|
14
|
+
--
|
|
15
|
+
-- MODULE-OWNED and `lifecycle_`-prefixed, so fitness check 11 permits it: a NEW
|
|
16
|
+
-- table the lifecycle module owns outright (ADR 0083 §Decision #5).
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS lifecycle_notes (
|
|
19
|
+
id bigserial PRIMARY KEY,
|
|
20
|
+
task_id bigint REFERENCES tasks(id),
|
|
21
|
+
goal_id bigint REFERENCES goals(id),
|
|
22
|
+
author_id bigint NOT NULL REFERENCES builders(id),
|
|
23
|
+
body text NOT NULL,
|
|
24
|
+
kind text NOT NULL DEFAULT 'other',
|
|
25
|
+
kind_source text NOT NULL DEFAULT 'auto',
|
|
26
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
27
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
28
|
+
|
|
29
|
+
CONSTRAINT lifecycle_notes_subject_chk
|
|
30
|
+
CHECK ((task_id IS NOT NULL) <> (goal_id IS NOT NULL)),
|
|
31
|
+
CONSTRAINT lifecycle_notes_body_chk
|
|
32
|
+
CHECK (length(btrim(body)) BETWEEN 1 AND 4000),
|
|
33
|
+
CONSTRAINT lifecycle_notes_kind_chk
|
|
34
|
+
CHECK (kind IN ('scope', 'decision', 'question', 'reference', 'progress', 'risk', 'other')),
|
|
35
|
+
CONSTRAINT lifecycle_notes_kind_source_chk
|
|
36
|
+
CHECK (kind_source IN ('author', 'auto'))
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- The two reads: everything on one task, everything on one goal, newest first.
|
|
40
|
+
CREATE INDEX IF NOT EXISTS lifecycle_notes_task_idx
|
|
41
|
+
ON lifecycle_notes (task_id, created_at DESC) WHERE task_id IS NOT NULL;
|
|
42
|
+
CREATE INDEX IF NOT EXISTS lifecycle_notes_goal_idx
|
|
43
|
+
ON lifecycle_notes (goal_id, created_at DESC) WHERE goal_id IS NOT NULL;
|
|
44
|
+
|
|
45
|
+
INSERT INTO schema_migrations (version) VALUES ('lifecycle_017_notes')
|
|
46
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// modules/lifecycle/notes.js — notes on a task or goal, grouped by purpose
|
|
2
|
+
// (task 1004745, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// A note is a short piece of information that helps scope a task or goal or helps
|
|
5
|
+
// finish it. A long thread of them reads badly, so they are shown GROUPED BY
|
|
6
|
+
// PURPOSE — what is decided, what is still a question, what to read, what is
|
|
7
|
+
// risky — with the newest few open in each group and the rest folded away.
|
|
8
|
+
//
|
|
9
|
+
// THE PURPOSE IS CHOSEN BY THE AUTHOR, AND GUESSED WHEN THEY DO NOT. Guessing by
|
|
10
|
+
// text likeness would need a model call per note and could regroup a reader's page
|
|
11
|
+
// under them; a stored purpose is stable, cheap and the author's own word. The
|
|
12
|
+
// guess (`suggestKind`) is a few plain rules (a link, a question mark, "decided",
|
|
13
|
+
// "risk") used only when no purpose was given, and is stored with
|
|
14
|
+
// kind_source 'auto' so it can be told from a deliberate choice.
|
|
15
|
+
//
|
|
16
|
+
// Reaches core only through the doorway (ADR 0083).
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const api = require('../../src/module-api');
|
|
21
|
+
|
|
22
|
+
const { pool } = api;
|
|
23
|
+
|
|
24
|
+
const MAX_BODY = 4000;
|
|
25
|
+
|
|
26
|
+
// The most notes one task or goal carries, so no one can bury a page in them.
|
|
27
|
+
const MAX_NOTES_PER_SUBJECT = 500;
|
|
28
|
+
|
|
29
|
+
// Display order is the order a person scoping work wants to read: what the work is,
|
|
30
|
+
// what was decided, what is still open, what is risky, how far along it is, what to
|
|
31
|
+
// read, and everything else last.
|
|
32
|
+
const KINDS = Object.freeze([
|
|
33
|
+
{ key: 'scope', label: 'Scope', hint: 'What is in and out' },
|
|
34
|
+
{ key: 'decision', label: 'Decisions', hint: 'What was settled, and why' },
|
|
35
|
+
{ key: 'question', label: 'Open questions', hint: 'What still needs an answer' },
|
|
36
|
+
{ key: 'risk', label: 'Risks', hint: 'What could go wrong' },
|
|
37
|
+
{ key: 'progress', label: 'Progress', hint: 'How far along it is' },
|
|
38
|
+
{ key: 'reference', label: 'Links & reading', hint: 'Where to look' },
|
|
39
|
+
{ key: 'other', label: 'Other', hint: 'Anything else' },
|
|
40
|
+
]);
|
|
41
|
+
const KIND_KEYS = Object.freeze(KINDS.map((k) => k.key));
|
|
42
|
+
|
|
43
|
+
// How many notes a group shows before folding the rest behind "show more".
|
|
44
|
+
const OPEN_PER_GROUP = 3;
|
|
45
|
+
|
|
46
|
+
// A purpose from the words, for a note written without one. Order matters: the
|
|
47
|
+
// first rule that matches wins, and 'other' is the honest answer when none does.
|
|
48
|
+
function suggestKind(body) {
|
|
49
|
+
const t = String(body == null ? '' : body).trim().toLowerCase();
|
|
50
|
+
if (!t) return 'other';
|
|
51
|
+
if (/\b(decided|decision|we will|we'll go with|going with|agreed|chose|chosen)\b/.test(t)) return 'decision';
|
|
52
|
+
if (/\b(risk|danger|could break|might break|breaks|careful|watch out|blocker|blocked)\b/.test(t)) return 'risk';
|
|
53
|
+
if (t.includes('?') || /^(how|what|why|should|can|could|does|do|is|are|who|when|where)\b/.test(t)) return 'question';
|
|
54
|
+
if (/\b(in scope|out of scope|scope|must|should include|needs to|acceptance|done when)\b/.test(t)) return 'scope';
|
|
55
|
+
if (/\b(done|finished|shipped|merged|started|working on|in progress|so far|next step)\b/.test(t)) return 'progress';
|
|
56
|
+
if (/https?:\/\/|\bsee (the )?(docs?|adr|spec|page|link)\b|\bdocs?\b|\badr\b|\bspec\b/.test(t)) return 'reference';
|
|
57
|
+
return 'other';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Which of the two subjects a request names: { taskId } or { goalId }, else null.
|
|
61
|
+
// Exactly one id must be present and a positive integer.
|
|
62
|
+
function subjectOf(src) {
|
|
63
|
+
const num = (v) => (v == null || v === '' ? null : (Number.isInteger(Number(v)) && Number(v) > 0 ? Number(v) : NaN));
|
|
64
|
+
const taskId = num(src.task_id);
|
|
65
|
+
const goalId = num(src.goal_id);
|
|
66
|
+
if (Number.isNaN(taskId) || Number.isNaN(goalId)) return null;
|
|
67
|
+
if ((taskId == null) === (goalId == null)) return null;
|
|
68
|
+
return taskId != null ? { taskId } : { goalId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate an incoming note; `kind` may be absent (then guessed).
|
|
72
|
+
function validateNote(input) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const body = input && typeof input.body === 'string' ? input.body.trim() : '';
|
|
75
|
+
if (!body) errors.push({ field: 'body', reason: 'required' });
|
|
76
|
+
else if (body.length > MAX_BODY) errors.push({ field: 'body', reason: `at most ${MAX_BODY} characters` });
|
|
77
|
+
const kind = input ? input.kind : undefined;
|
|
78
|
+
if (kind != null && !KIND_KEYS.includes(kind)) errors.push({ field: 'kind', reason: `pick a group: ${KINDS.map((k) => k.label).join(', ')}` });
|
|
79
|
+
if (!subjectOf(input || {})) errors.push({ field: 'subject', reason: 'say whether the note is on a task or on a goal, not both' });
|
|
80
|
+
if (errors.length) return { ok: false, errors };
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
body,
|
|
84
|
+
kind: kind == null ? suggestKind(body) : kind,
|
|
85
|
+
kindSource: kind == null ? 'auto' : 'author',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rows (newest first, or any order) -> the grouped view the page renders. Empty
|
|
90
|
+
// groups are left out; within a group newest comes first; `open` is what shows
|
|
91
|
+
// at once and `folded` is what waits behind "show more".
|
|
92
|
+
function groupNotes(rows, { openPerGroup = OPEN_PER_GROUP } = {}) {
|
|
93
|
+
const byKind = new Map(KIND_KEYS.map((k) => [k, []]));
|
|
94
|
+
for (const r of rows || []) {
|
|
95
|
+
const k = byKind.has(r.kind) ? r.kind : 'other';
|
|
96
|
+
byKind.get(k).push(r);
|
|
97
|
+
}
|
|
98
|
+
const groups = [];
|
|
99
|
+
for (const meta of KINDS) {
|
|
100
|
+
const list = byKind.get(meta.key).slice().sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
101
|
+
if (!list.length) continue;
|
|
102
|
+
groups.push({
|
|
103
|
+
kind: meta.key,
|
|
104
|
+
label: meta.label,
|
|
105
|
+
hint: meta.hint,
|
|
106
|
+
count: list.length,
|
|
107
|
+
latest_at: list[0].created_at,
|
|
108
|
+
open: list.slice(0, openPerGroup),
|
|
109
|
+
folded: list.slice(openPerGroup),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return groups;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const COLS = 'n.id, n.task_id, n.goal_id, n.author_id, n.body, n.kind, n.kind_source, n.created_at, n.updated_at, '
|
|
116
|
+
+ 'b.github_login AS author_login, b.display_name AS author_name, b.avatar_url AS author_avatar_url';
|
|
117
|
+
|
|
118
|
+
// Every note on one task or goal, newest first. `subject` is { taskId } or { goalId }.
|
|
119
|
+
async function listNotes(subject, deps = {}) {
|
|
120
|
+
const db = deps.pool || pool;
|
|
121
|
+
const col = subject.taskId != null ? 'n.task_id' : 'n.goal_id';
|
|
122
|
+
const id = subject.taskId != null ? subject.taskId : subject.goalId;
|
|
123
|
+
const { rows } = await db.query(
|
|
124
|
+
`SELECT ${COLS} FROM lifecycle_notes n JOIN builders b ON b.id = n.author_id
|
|
125
|
+
WHERE ${col} = $1 ORDER BY n.created_at DESC, n.id DESC LIMIT $2`,
|
|
126
|
+
[id, MAX_NOTES_PER_SUBJECT]
|
|
127
|
+
);
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// How many notes a task or goal already carries (for the per-subject cap).
|
|
132
|
+
async function countNotes(subject, deps = {}) {
|
|
133
|
+
const db = deps.pool || pool;
|
|
134
|
+
const col = subject.taskId != null ? 'task_id' : 'goal_id';
|
|
135
|
+
const id = subject.taskId != null ? subject.taskId : subject.goalId;
|
|
136
|
+
const { rows } = await db.query(`SELECT count(*)::int AS n FROM lifecycle_notes WHERE ${col} = $1`, [id]);
|
|
137
|
+
return rows[0].n;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function addNote({ taskId = null, goalId = null, authorId, body, kind, kindSource }, deps = {}) {
|
|
141
|
+
const db = deps.pool || pool;
|
|
142
|
+
const { rows } = await db.query(
|
|
143
|
+
`WITH ins AS (
|
|
144
|
+
INSERT INTO lifecycle_notes (task_id, goal_id, author_id, body, kind, kind_source)
|
|
145
|
+
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *
|
|
146
|
+
)
|
|
147
|
+
SELECT ${COLS} FROM ins n JOIN builders b ON b.id = n.author_id`,
|
|
148
|
+
[taskId, goalId, authorId, body, kind, kindSource]
|
|
149
|
+
);
|
|
150
|
+
return rows[0];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Re-file a note under another purpose. Only its author or a Metic+ may (the route
|
|
154
|
+
// decides who; this just writes), and the choice is now the author's.
|
|
155
|
+
async function setKind(id, kind, deps = {}) {
|
|
156
|
+
const db = deps.pool || pool;
|
|
157
|
+
const { rows } = await db.query(
|
|
158
|
+
`UPDATE lifecycle_notes SET kind = $2, kind_source = 'author', updated_at = now()
|
|
159
|
+
WHERE id = $1 RETURNING id, kind, kind_source, author_id`,
|
|
160
|
+
[id, kind]
|
|
161
|
+
);
|
|
162
|
+
return rows[0] || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function getNote(id, deps = {}) {
|
|
166
|
+
const db = deps.pool || pool;
|
|
167
|
+
const { rows } = await db.query('SELECT id, author_id, kind, task_id, goal_id FROM lifecycle_notes WHERE id = $1', [id]);
|
|
168
|
+
return rows[0] || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function deleteNote(id, deps = {}) {
|
|
172
|
+
const db = deps.pool || pool;
|
|
173
|
+
const { rowCount } = await db.query('DELETE FROM lifecycle_notes WHERE id = $1', [id]);
|
|
174
|
+
return rowCount > 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// May this builder see notes on this task or goal? A private goal's notes (and those of
|
|
178
|
+
// its tasks) are for its members and an Archon only, the same wall the goal's own title
|
|
179
|
+
// and prose sit behind (ADR 0112). Anything else is open to every signed-in builder.
|
|
180
|
+
// A task or goal that does not exist answers false too, so the route gives the same
|
|
181
|
+
// 404 for "missing" and "not yours to see" from ONE read.
|
|
182
|
+
async function canSee(builder, subject, deps = {}) {
|
|
183
|
+
const db = deps.pool || pool;
|
|
184
|
+
const { rows } = subject.taskId != null
|
|
185
|
+
? await db.query('SELECT g.id AS goal_id, g.visibility FROM tasks t LEFT JOIN goals g ON g.id = t.goal_id WHERE t.id = $1', [subject.taskId])
|
|
186
|
+
: await db.query('SELECT g.id AS goal_id, g.visibility FROM goals g WHERE g.id = $1', [subject.goalId]);
|
|
187
|
+
const g = rows[0];
|
|
188
|
+
if (!g) return false;
|
|
189
|
+
if (g.visibility !== 'private' || api.rankMeetsThreshold(builder.rank, ['archon'])) return true;
|
|
190
|
+
const m = await db.query('SELECT 1 FROM goal_members WHERE goal_id = $1 AND builder_id = $2', [g.goal_id, builder.id]);
|
|
191
|
+
return m.rows.length > 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Who may re-file or delete a note: its author, or a Metic or above. The rank is the live
|
|
195
|
+
// one on the authenticated request (ADR 0016), compared through the kernel's own ladder.
|
|
196
|
+
function canChange(builder, note) {
|
|
197
|
+
if (String(builder.id) === String(note.author_id)) return true;
|
|
198
|
+
return api.rankMeetsThreshold(builder.rank, ['metic', 'archon']);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
KINDS,
|
|
203
|
+
MAX_NOTES_PER_SUBJECT,
|
|
204
|
+
subjectOf,
|
|
205
|
+
canSee,
|
|
206
|
+
canChange,
|
|
207
|
+
countNotes,
|
|
208
|
+
KIND_KEYS,
|
|
209
|
+
MAX_BODY,
|
|
210
|
+
suggestKind,
|
|
211
|
+
validateNote,
|
|
212
|
+
groupNotes,
|
|
213
|
+
listNotes,
|
|
214
|
+
|
|
215
|
+
addNote,
|
|
216
|
+
setKind,
|
|
217
|
+
getNote,
|
|
218
|
+
deleteNote,
|
|
219
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// modules/lifecycle/page-notice.js — WHO IS UPDATING A PAGE, in one read (task 1004948).
|
|
2
|
+
//
|
|
3
|
+
// An artist's page claim is a marker (task 1004947), so nothing blocks anyone;
|
|
4
|
+
// this is the other half: telling people. `heldPagesForTouches` answers, for the
|
|
5
|
+
// files a task touches, which pages an artist is updating right now, who, and
|
|
6
|
+
// since when. `engineeringActiveOnPage` answers the reverse for an artist: whose
|
|
7
|
+
// engineering claims are open on files that page is made of. Information only:
|
|
8
|
+
// nothing here refuses, gates or writes, and a failed read answers "no notice"
|
|
9
|
+
// rather than throwing, because a heads-up must never break a claim.
|
|
10
|
+
//
|
|
11
|
+
// Every notice is `{ page_id, holder, since, task_id, line }`, the same shape on
|
|
12
|
+
// every surface (claim output, GET /me, the Conductor). Names and ids that reach a
|
|
13
|
+
// session as text are cleaned first: a display name is somebody else's words.
|
|
14
|
+
// Engineering work is reported by holder only, never by title: a task's title can
|
|
15
|
+
// belong to a goal the asking artist may not read (ADR 0112).
|
|
16
|
+
//
|
|
17
|
+
// Reads through the caller's executor (a pool or a transaction client) and the
|
|
18
|
+
// doorway's path matcher, like the rest of the claim ladder.
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const api = require('../../src/module-api');
|
|
23
|
+
const { touchesOverlap } = api.pathMatch;
|
|
24
|
+
const { PAGE_TWEAK_SOURCE, webClaimPredicate } = require('./page-tweak-artist.js');
|
|
25
|
+
|
|
26
|
+
// Open page markers / claims are bounded by concurrent people; the cap is a backstop.
|
|
27
|
+
const ROW_CAP = 500;
|
|
28
|
+
|
|
29
|
+
// 'page-tweak/builders:studio/r2' -> 'builders:studio' ('' when malformed).
|
|
30
|
+
function pageIdOf(sourceRef) {
|
|
31
|
+
const m = /^page-tweak\/([a-z0-9:-]{1,80})\/r[1-9][0-9]*$/.exec(String(sourceRef || ''));
|
|
32
|
+
return m ? m[1] : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A name that is safe to print into a session's context: no control characters,
|
|
36
|
+
// no newlines, at most 60 characters.
|
|
37
|
+
function cleanName(raw, fallback) {
|
|
38
|
+
const s = String(raw == null ? '' : raw).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
39
|
+
return s || fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// One line a person or an AI session reads: gentle, never a refusal.
|
|
43
|
+
function noticeLine(n) {
|
|
44
|
+
const since = n.since ? new Date(n.since).toISOString().slice(0, 10) : 'recently';
|
|
45
|
+
return `${n.page_id} is being updated by ${n.holder} (since ${since}) — expect copy changes on it.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Every open page marker, with its holder: ONE read for any number of tasks.
|
|
49
|
+
async function openMarkers(exec) {
|
|
50
|
+
const { rows } = await exec.query(
|
|
51
|
+
`SELECT t.id AS task_id, t.source_ref, t.touches, c.claimed_at,
|
|
52
|
+
COALESCE(b.display_name, b.github_login) AS holder
|
|
53
|
+
FROM claims c
|
|
54
|
+
JOIN tasks t ON t.id = c.task_id
|
|
55
|
+
JOIN builders b ON b.id = c.builder_id
|
|
56
|
+
WHERE c.released_at IS NULL
|
|
57
|
+
AND t.source = '${PAGE_TWEAK_SOURCE}'
|
|
58
|
+
AND ${webClaimPredicate('c')}
|
|
59
|
+
ORDER BY c.claimed_at
|
|
60
|
+
LIMIT ${ROW_CAP}`
|
|
61
|
+
);
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function noticesFor(touches, markers, exceptTaskId) {
|
|
66
|
+
if (!Array.isArray(touches) || touches.length === 0) return [];
|
|
67
|
+
return markers
|
|
68
|
+
.filter((r) => String(r.task_id) !== String(exceptTaskId) && touchesOverlap(touches, r.touches))
|
|
69
|
+
.map((r) => {
|
|
70
|
+
const n = { page_id: pageIdOf(r.source_ref), holder: cleanName(r.holder, 'an artist'), since: r.claimed_at, task_id: String(r.task_id) };
|
|
71
|
+
return { ...n, line: noticeLine(n) };
|
|
72
|
+
})
|
|
73
|
+
.filter((n) => n.page_id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// heldPagesForTouches(touches, exec, { exceptTaskId }) -> notices (empty on no overlap or any failure)
|
|
77
|
+
async function heldPagesForTouches(touches, exec, { exceptTaskId = null } = {}) {
|
|
78
|
+
if (!Array.isArray(touches) || touches.length === 0) return [];
|
|
79
|
+
try {
|
|
80
|
+
return noticesFor(touches, await openMarkers(exec), exceptTaskId);
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// noticesForClaims(claims, exec) -> Map(task_id -> notices): ONE read for all of a
|
|
87
|
+
// builder's claims (GET /me), instead of one per claim. A claim with no touches or
|
|
88
|
+
// no overlap maps to [].
|
|
89
|
+
async function noticesForClaims(claims, exec = api.pool) {
|
|
90
|
+
const out = new Map();
|
|
91
|
+
const list = Array.isArray(claims) ? claims : [];
|
|
92
|
+
for (const c of list) out.set(String(c.task_id), []);
|
|
93
|
+
if (list.length === 0) return out;
|
|
94
|
+
try {
|
|
95
|
+
const markers = await openMarkers(exec);
|
|
96
|
+
for (const c of list) out.set(String(c.task_id), noticesFor(c.touches, markers, c.task_id));
|
|
97
|
+
} catch (_) { /* heads-up only */ }
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// engineeringActiveOnPage(pageTouches, exec) -> [{ task_id, holder }]
|
|
102
|
+
// Open worktree claims on real code work (never a page round, whoever holds it)
|
|
103
|
+
// whose files overlap the page's files. No title: see the header.
|
|
104
|
+
async function engineeringActiveOnPage(pageTouches, exec = api.pool) {
|
|
105
|
+
if (!Array.isArray(pageTouches) || pageTouches.length === 0) return [];
|
|
106
|
+
try {
|
|
107
|
+
const { rows } = await exec.query(
|
|
108
|
+
`SELECT t.id AS task_id, t.touches, COALESCE(b.display_name, b.github_login) AS holder
|
|
109
|
+
FROM claims c
|
|
110
|
+
JOIN tasks t ON t.id = c.task_id
|
|
111
|
+
JOIN builders b ON b.id = c.builder_id
|
|
112
|
+
WHERE c.released_at IS NULL
|
|
113
|
+
AND t.source IS DISTINCT FROM '${PAGE_TWEAK_SOURCE}'
|
|
114
|
+
LIMIT ${ROW_CAP}`
|
|
115
|
+
);
|
|
116
|
+
return rows
|
|
117
|
+
.filter((r) => touchesOverlap(pageTouches, r.touches))
|
|
118
|
+
.map((r) => ({ task_id: String(r.task_id), holder: cleanName(r.holder, 'an engineer') }));
|
|
119
|
+
} catch (_) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { heldPagesForTouches, noticesForClaims, engineeringActiveOnPage };
|
|
@@ -24,6 +24,23 @@ function webClaimPredicate(alias = 'c') {
|
|
|
24
24
|
return `${alias}.worktree_name IS NULL AND ${alias}.creator_session_id IS NULL`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// A PAGE MARKER is an artist's web claim on a page-tweak round: it says "this
|
|
28
|
+
// page is being updated" and nothing more. The file-overlap checks that guard
|
|
29
|
+
// engineering work against engineering work (the claimable feed, claimTask,
|
|
30
|
+
// the batch claim and its dry-run) leave markers out, so an artist writing a
|
|
31
|
+
// page never hides or refuses coding work on the same files. That includes
|
|
32
|
+
// claimTask's module-disjointness gate, which reads the same active-claim rows:
|
|
33
|
+
// a page round carries no module_key, and a marker must never hold a module.
|
|
34
|
+
// Returns a SQL condition that is true for every claim that is NOT a marker.
|
|
35
|
+
// COALESCE keeps it TRUE (counted) when tasks.source is NULL: without it a
|
|
36
|
+
// both-NULL claim on a source-less task would evaluate to NULL and vanish.
|
|
37
|
+
// src/bongos/optimizer.js carries a hand copy (core cannot require a module);
|
|
38
|
+
// tests/page_tweak_marker_sql.mjs pins the two together.
|
|
39
|
+
// Aliases are code literals at every call site; never pass request input.
|
|
40
|
+
function notPageMarkerSql(claimAlias = 'c', taskAlias = 't') {
|
|
41
|
+
return `NOT (COALESCE(${taskAlias}.source, '') = '${PAGE_TWEAK_SOURCE}' AND ${webClaimPredicate(claimAlias)})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
// The newest claim first; the id breaks a same-instant tie the same way in
|
|
28
45
|
// every reader.
|
|
29
46
|
function latestFirst(alias = 'c') {
|
|
@@ -83,4 +100,4 @@ function helperJoinSql(alias = 't') {
|
|
|
83
100
|
) helper ON true`;
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
|
|
103
|
+
module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, notPageMarkerSql, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
|