@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.
Files changed (424) hide show
  1. package/.bongos-core.json +902 -407
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +90 -0
  27. package/clients/bongos-client/index.cjs +90 -0
  28. package/clients/bongos-client/index.d.ts +149 -4
  29. package/clients/bongos-client/index.mjs +90 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  33. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  34. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  35. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  36. package/docs/adr/README.md +5 -1
  37. package/docs/api/openapi.json +8555 -5876
  38. package/docs/api-reference.md +63 -10
  39. package/docs/architecture.md +37 -1
  40. package/docs/canonical-permissions.md +1 -1
  41. package/docs/copy-inventory.md +420 -337
  42. package/docs/copy-registry.json +1321 -500
  43. package/docs/file-map.md +1 -0
  44. package/docs/module-api-changelog.md +5 -0
  45. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  46. package/docs/onboarding/diagrams/README.md +1 -1
  47. package/docs/onboarding/drachmae-and-karma.md +15 -15
  48. package/docs/onboarding/primer.md +2 -2
  49. package/docs/page-inventory.json +55 -6
  50. package/docs/page-readings.json +1185 -1011
  51. package/docs/recipes/google-sign-in.md +60 -0
  52. package/docs/recipes/ops-gotchas.md +1 -1
  53. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  54. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  55. package/migrations/core_283_payout_approvals.sql +183 -0
  56. package/migrations/core_284_payout_gates.sql +179 -0
  57. package/migrations/core_285_task_needs.sql +74 -0
  58. package/modules/ai-studio/billing.js +67 -0
  59. package/modules/ai-studio/context.js +29 -0
  60. package/modules/ai-studio/db.js +172 -0
  61. package/modules/ai-studio/gemini.js +75 -0
  62. package/modules/ai-studio/generate.js +119 -0
  63. package/modules/ai-studio/keys.js +73 -0
  64. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  65. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  66. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  67. package/modules/ai-studio/openrouter.js +90 -0
  68. package/modules/ai-studio/payments.js +105 -0
  69. package/modules/ai-studio/pricing.js +92 -0
  70. package/modules/ai-studio/routes/ai-studio.js +235 -5
  71. package/modules/ai-studio/studio.js +121 -0
  72. package/modules/autonomy/fence.js +64 -1
  73. package/modules/builder-settings/builder-needs.js +27 -1
  74. package/modules/copy-desk/routes/copy-desk.js +5 -0
  75. package/modules/economy/achievements.js +22 -10
  76. package/modules/economy/cost-classify.js +9 -0
  77. package/modules/economy/cost-write-guard.js +52 -0
  78. package/modules/economy/cost.js +2 -1
  79. package/modules/economy/credits.js +18 -15
  80. package/modules/economy/goal-reward.js +161 -0
  81. package/modules/economy/reward.js +4 -0
  82. package/modules/economy/routes/cost.js +33 -7
  83. package/modules/government/catalog.js +6 -0
  84. package/modules/government/db.js +34 -0
  85. package/modules/government/docket.js +36 -0
  86. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  87. package/modules/government/pollers/board-expiry.js +108 -20
  88. package/modules/government/protected-surfaces.json +1 -0
  89. package/modules/government/routes/government.js +22 -0
  90. package/modules/government/routing.js +39 -5
  91. package/modules/government/waiting.js +74 -0
  92. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  93. package/modules/hall-ui/public/ai-studio.css +193 -0
  94. package/modules/hall-ui/public/ai-studio.html +98 -0
  95. package/modules/hall-ui/public/ai-studio.js +556 -0
  96. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  97. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  98. package/modules/hall-ui/public/blockers-lib.js +15 -1
  99. package/modules/hall-ui/public/blockers.html +8 -0
  100. package/modules/hall-ui/public/blockers.js +1 -1
  101. package/modules/hall-ui/public/claim-action.js +3 -0
  102. package/modules/hall-ui/public/collab.html +12 -0
  103. package/modules/hall-ui/public/craft-mode.js +4 -1
  104. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  105. package/modules/hall-ui/public/drachmae.html +1 -1
  106. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  107. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  108. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  109. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  110. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  111. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  115. package/modules/hall-ui/public/goal-inbox.js +1 -1
  116. package/modules/hall-ui/public/goals-render.js +4 -0
  117. package/modules/hall-ui/public/goals.html +8 -0
  118. package/modules/hall-ui/public/gov-living.js +6 -2
  119. package/modules/hall-ui/public/hall-ask.css +5 -0
  120. package/modules/hall-ui/public/hall-ask.js +39 -3
  121. package/modules/hall-ui/public/notes-panel.css +20 -0
  122. package/modules/hall-ui/public/notes-panel.js +146 -0
  123. package/modules/hall-ui/public/project-settings.html +1 -1
  124. package/modules/hall-ui/public/project-settings.js +7 -5
  125. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  126. package/modules/hall-ui/public/settings.html +15 -0
  127. package/modules/hall-ui/public/street-deck.js +12 -3
  128. package/modules/hall-ui/public/studio.css +90 -8
  129. package/modules/hall-ui/public/studio.html +39 -10
  130. package/modules/hall-ui/public/studio.js +148 -18
  131. package/modules/hall-ui/public/task.html +2 -0
  132. package/modules/hall-ui/public/task.js +4 -0
  133. package/modules/hall-ui/public/tweak-editor.js +12 -3
  134. package/modules/hall-ui/public/welcome-boot.css +42 -6
  135. package/modules/hall-ui/public/welcome-boot.js +20 -6
  136. package/modules/hall-ui/public/work.js +20 -0
  137. package/modules/hall-ui/records/ai-studio.md +36 -0
  138. package/modules/hall-ui/records/blockers-people.md +24 -0
  139. package/modules/lifecycle/db-claims.js +32 -4
  140. package/modules/lifecycle/db-goals.js +18 -5
  141. package/modules/lifecycle/db-tasks.js +15 -6
  142. package/modules/lifecycle/done-when.js +23 -1
  143. package/modules/lifecycle/lifecycle.js +5 -0
  144. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  145. package/modules/lifecycle/module.json +2 -1
  146. package/modules/lifecycle/notes.js +219 -0
  147. package/modules/lifecycle/page-notice.js +124 -0
  148. package/modules/lifecycle/page-tweak-artist.js +18 -1
  149. package/modules/lifecycle/publish-reconciler.js +67 -20
  150. package/modules/lifecycle/routes/claims.js +5 -2
  151. package/modules/lifecycle/routes/goals.js +3 -1
  152. package/modules/lifecycle/routes/notes.js +91 -0
  153. package/modules/lifecycle/routes/project-settings.js +43 -35
  154. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  155. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  156. package/modules/lifecycle/routes/tasks.js +12 -12
  157. package/modules/lifecycle/task-needs-gate.js +70 -0
  158. package/modules/market-watch/CLAUDE.md +15 -0
  159. package/modules/market-watch/db.js +340 -0
  160. package/modules/market-watch/key.js +58 -0
  161. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  162. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  163. package/modules/market-watch/module.json +26 -0
  164. package/modules/market-watch/pollers/hub-seed.js +54 -0
  165. package/modules/market-watch/pollers/weekly-run.js +100 -0
  166. package/modules/market-watch/research.js +350 -0
  167. package/modules/market-watch/run.js +245 -0
  168. package/modules/market-watch/seed/market-data.json +38 -0
  169. package/modules/market-watch/seed/partners-data.json +497 -0
  170. package/modules/market-watch/seed.js +252 -0
  171. package/modules/market-watch/store.js +230 -0
  172. package/modules/memory/routes/memory.js +14 -2
  173. package/modules/memory/routes/search.js +8 -2
  174. package/modules/platform-identity/account-logins.js +192 -0
  175. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  176. package/modules/platform-identity/platform-identity.js +5 -4
  177. package/modules/platform-identity/routes/my-projects.js +6 -0
  178. package/modules/platform-identity/routes/profile.js +18 -0
  179. package/modules/platform-identity/routes/public-profile.js +10 -1
  180. package/modules/platform-identity/routes/sso.js +26 -1
  181. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  182. package/modules/provisioning/credential-preflight.js +13 -3
  183. package/modules/provisioning/pollers/app-liveness.js +25 -5
  184. package/modules/provisioning/routes/provisioning.js +1 -1
  185. package/modules/provisioning/starter-bundles.js +6 -2
  186. package/modules/public-landing/public/account.html +10 -2
  187. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  188. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  189. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  190. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  191. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  192. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  193. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  194. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  195. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  196. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  197. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  198. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  199. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  200. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  201. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  202. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  203. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  204. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  205. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  206. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  207. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  208. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  209. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  210. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  211. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  212. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  213. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  214. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  215. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  216. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  217. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  218. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  219. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  220. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  221. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  222. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  223. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  224. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  225. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  226. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  227. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  228. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  229. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  230. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  231. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  232. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  233. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  234. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  235. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  236. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  237. package/modules/public-landing/public/community.html +1 -1
  238. package/modules/public-landing/public/connections.html +2 -2
  239. package/modules/public-landing/public/guild.html +1 -1
  240. package/modules/public-landing/public/invitations.html +2 -2
  241. package/modules/public-landing/public/projects.html +5 -5
  242. package/modules/raise/views/nda.html +2 -0
  243. package/modules/specialities/db.js +32 -1
  244. package/modules/specialities/routes/specialities.js +10 -6
  245. package/modules/status-ui/public/index.html +1 -1
  246. package/package-lock.json +2 -2
  247. package/package.json +1 -1
  248. package/release-notes.json +220 -0
  249. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  250. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  251. package/scripts/bongos/claim.js +22 -1
  252. package/scripts/bongos/db-backup-nightly.sh +29 -0
  253. package/scripts/bongos/deploy-divergence.js +7 -0
  254. package/scripts/bongos/doc-cli-guard.js +6 -0
  255. package/scripts/bongos/fitness.js +3 -2
  256. package/scripts/bongos/gen-diagrams.js +3 -3
  257. package/scripts/bongos/lib.sh +4 -5
  258. package/scripts/bongos/migration-namespace.js +13 -1
  259. package/scripts/bongos/plain-cards.js +4 -0
  260. package/scripts/bongos/provision-github-setup.js +3 -1
  261. package/scripts/bongos/provision-units.js +6 -1
  262. package/scripts/bongos/publish-manifest.js +3 -0
  263. package/scripts/bongos/release.js +2 -0
  264. package/scripts/bongos/run-unit-tests.js +268 -82
  265. package/scripts/bongos/seed-tasks.example.json +56 -0
  266. package/scripts/bongos/seed-tasks.js +222 -0
  267. package/scripts/bongos/seed-verify-criteria.js +1 -2
  268. package/scripts/bongos/ship-finish.js +2 -0
  269. package/scripts/bongos/ship-resume.js +2 -0
  270. package/scripts/bongos/strand-lease.js +250 -0
  271. package/scripts/bongos/strand-watch.js +26 -5
  272. package/scripts/bongos/version-literal-guard.js +2 -2
  273. package/scripts/discord/post.js +4 -3
  274. package/scripts/gds/seed-tasks.js +7 -0
  275. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  276. package/scripts/hall-preview/server.js +21 -0
  277. package/src/bongos/auth-config.js +29 -1
  278. package/src/bongos/auth-discord.js +3 -3
  279. package/src/bongos/auth-github.js +3 -6
  280. package/src/bongos/auth-google.js +217 -0
  281. package/src/bongos/device-approval.js +140 -0
  282. package/src/bongos/llm-pricing.js +17 -1
  283. package/src/bongos/metrics.js +48 -2
  284. package/src/bongos/middleware/rate-limit.js +30 -11
  285. package/src/bongos/module-scope-map.js +12 -1
  286. package/src/bongos/optimizer.js +15 -1
  287. package/src/bongos/payout-approvals.js +342 -0
  288. package/src/bongos/payout-gates.js +364 -0
  289. package/src/bongos/prelaunch-gate.js +7 -9
  290. package/src/bongos/prelaunch-switch.js +20 -0
  291. package/src/bongos/route-rank-check.js +3 -0
  292. package/src/bongos/routes/auth-google.js +246 -0
  293. package/src/bongos/routes/auth.js +38 -1
  294. package/src/bongos/routes/backup.js +33 -9
  295. package/src/bongos/routes/builders.js +57 -30
  296. package/src/bongos/routes/github.js +9 -0
  297. package/src/bongos/routes/me.js +18 -0
  298. package/src/bongos/routes/my-payouts.js +133 -0
  299. package/src/bongos/routes/payout-approvals.js +312 -0
  300. package/src/bongos/routes/payout-gates.js +179 -0
  301. package/src/bongos/routes.js +15 -1
  302. package/src/bongos/secret-box.js +18 -3
  303. package/src/bongos/serve-internal.js +47 -19
  304. package/src/bongos/sign-in-pages.js +138 -0
  305. package/src/bongos/task-needs.js +146 -0
  306. package/src/bongos-downloads.js +18 -1
  307. package/src/instance-config.js +28 -0
  308. package/src/module-api.js +14 -1
  309. package/tests/account_logins.mjs +326 -0
  310. package/tests/account_logins_db.mjs +161 -0
  311. package/tests/ai_studio_billing.mjs +253 -0
  312. package/tests/ai_studio_generate.mjs +282 -0
  313. package/tests/ai_studio_keys.mjs +159 -0
  314. package/tests/ai_studio_requests.mjs +319 -0
  315. package/tests/auth_backchannel_logout_route.mjs +201 -0
  316. package/tests/auto_satisfy_criteria.mjs +10 -1
  317. package/tests/autobongos_tweak_lane.mjs +144 -0
  318. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  319. package/tests/backfill_subagent_rewards.mjs +17 -1
  320. package/tests/backup_trigger_lock.mjs +119 -0
  321. package/tests/blockers_people.mjs +198 -0
  322. package/tests/bounded_rate_maps.mjs +184 -0
  323. package/tests/claimable_blocker_status.mjs +1 -1
  324. package/tests/claims_batch_routes.mjs +421 -0
  325. package/tests/config_read_cache.mjs +171 -0
  326. package/tests/copy_desk_page_editor.mjs +12 -0
  327. package/tests/cost_write_guard.mjs +200 -0
  328. package/tests/craft_achievements.mjs +33 -10
  329. package/tests/criterion_goal_attach.mjs +1 -1
  330. package/tests/db_backup_nightly.mjs +73 -1
  331. package/tests/deploy_divergence_line.mjs +7 -0
  332. package/tests/effective_visibility_predicate.mjs +12 -0
  333. package/tests/fitness.mjs +19 -0
  334. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  335. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  336. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  337. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  338. package/tests/github_one_trip.mjs +34 -0
  339. package/tests/github_repo_visibility.mjs +20 -0
  340. package/tests/goal_achievement.mjs +30 -1
  341. package/tests/goal_achievement_reward.mjs +205 -0
  342. package/tests/goal_sort_staleness.mjs +2 -1
  343. package/tests/goal_tier.mjs +11 -1
  344. package/tests/google_fonts_nonblocking.mjs +84 -0
  345. package/tests/google_sign_in.mjs +401 -0
  346. package/tests/government_board_expiry.mjs +94 -0
  347. package/tests/government_protected_surfaces.mjs +1 -1
  348. package/tests/government_routes.mjs +1 -1
  349. package/tests/hall_ai_studio.mjs +347 -0
  350. package/tests/hall_page_gate_map.mjs +3 -0
  351. package/tests/hall_studio_home.mjs +61 -2
  352. package/tests/home_attention_cards.mjs +1 -1
  353. package/tests/human_signal.mjs +141 -0
  354. package/tests/install_script_reader.mjs +51 -0
  355. package/tests/integration_fixture_ids.mjs +82 -0
  356. package/tests/invite_expiry_db.mjs +13 -1
  357. package/tests/lib_sh_resolution.mjs +0 -4
  358. package/tests/market_watch_data.mjs +244 -0
  359. package/tests/market_watch_data_db.mjs +308 -0
  360. package/tests/market_watch_module.mjs +114 -0
  361. package/tests/market_watch_run.mjs +503 -0
  362. package/tests/metrics.mjs +124 -0
  363. package/tests/migration_namespace.mjs +14 -0
  364. package/tests/module_api.mjs +2 -0
  365. package/tests/module_loader.mjs +1 -1
  366. package/tests/notes.mjs +182 -0
  367. package/tests/own_scope_guard.mjs +5 -0
  368. package/tests/page_tweak_marker_sql.mjs +76 -0
  369. package/tests/page_tweak_no_block.mjs +135 -0
  370. package/tests/page_tweak_notice.mjs +103 -0
  371. package/tests/payout_approvals.mjs +253 -0
  372. package/tests/payout_approvals_db.mjs +106 -0
  373. package/tests/payout_gates.mjs +322 -0
  374. package/tests/payout_gates_db.mjs +113 -0
  375. package/tests/planning_session_skill.mjs +13 -3
  376. package/tests/project_settings_page.mjs +29 -5
  377. package/tests/project_settings_spend_cap_route.mjs +235 -0
  378. package/tests/promotion_sweep.mjs +29 -1
  379. package/tests/provision.mjs +3 -2
  380. package/tests/provisioning_app_status.mjs +18 -0
  381. package/tests/provisioning_credential_preflight.mjs +53 -0
  382. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  383. package/tests/publish_reconciler_overlap.mjs +60 -0
  384. package/tests/rank_change_broadcast_port.mjs +151 -0
  385. package/tests/seed_tasks.mjs +176 -0
  386. package/tests/seed_verify_criteria.mjs +2 -6
  387. package/tests/site_classified_once.mjs +142 -0
  388. package/tests/speciality_db_shape.mjs +23 -0
  389. package/tests/speciality_session_skills.mjs +48 -2
  390. package/tests/strand_lease.mjs +249 -0
  391. package/tests/strand_watch.mjs +10 -0
  392. package/tests/task_criteria_routes.mjs +275 -0
  393. package/tests/task_criteria_unlink_db.mjs +123 -0
  394. package/tests/task_needs.mjs +233 -0
  395. package/tests/task_needs_db.mjs +110 -0
  396. package/tests/unit_runner_serial.mjs +266 -43
  397. package/tests/version_literals.mjs +5 -7
  398. package/tests/welcome_boot.mjs +16 -5
  399. package/tests/welcome_boot_browser.mjs +36 -4
  400. package/docs/recipes/bongos-cli-release.md +0 -66
  401. package/scripts/bongos/backfill-disciplines.js +0 -144
  402. package/scripts/bongos/backfill-session-earnings.js +0 -123
  403. package/scripts/bongos/backfill-session-types.js +0 -87
  404. package/scripts/bongos/build-bongos.js +0 -94
  405. package/scripts/bongos/cleanup-orphans.js +0 -74
  406. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  407. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  408. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  409. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  410. package/scripts/bongos/smoke-batch-release.sh +0 -316
  411. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  412. package/scripts/bongos/smoke-dependencies.sh +0 -143
  413. package/scripts/gds/backfill-disciplines.js +0 -7
  414. package/scripts/gds/backfill-session-earnings.js +0 -7
  415. package/scripts/gds/backfill-session-types.js +0 -7
  416. package/scripts/gds/cleanup-orphans.js +0 -7
  417. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  418. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  419. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  420. package/scripts/gds/smoke-batch-claim.sh +0 -8
  421. package/scripts/gds/smoke-batch-release.sh +0 -8
  422. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  423. package/scripts/gds/smoke-dependencies.sh +0 -8
  424. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,364 @@
1
+ 'use strict';
2
+
3
+ // src/bongos/payout-gates.js — who may be paid at all: age tier, sanctions
4
+ // screening, and a freeze that tells the person (task 1004914, goal 1000092 N10;
5
+ // ADR 0385 decision 8).
6
+ //
7
+ // WHY. Two gates stand in front of every payout in the owner's legal framework
8
+ // (cited by Article only). Art. VII, age: under 13 takes no part in pay; 13 to 15
9
+ // is paid no money at all; 16 and 17 only with a guardian's co-signature and into
10
+ // an account the guardian controls. Art. X, sanctions: the recipient is screened
11
+ // and a match stops the payout. A stop that touches money someone is owed must
12
+ // tell them and offer an appeal — pay is never withheld silently.
13
+ //
14
+ // HOW IT IS USED. gateRecipient() runs on EVERY money route (the N9 approval
15
+ // routes in routes/payout-approvals.js, and checkRun / checkApproval, which the
16
+ // paying tasks M04, N6 and N7 call at pay time) — in the API, not the screen. When
17
+ // it refuses money that is owed, holdOwedPay() files a freeze whose notice and
18
+ // appeal route are NOT NULL in the table, so a silent stop cannot be stored; the
19
+ // person reads it at GET /me/payouts and appeals it there.
20
+ //
21
+ // WHAT WE DO NOT RELY ON. The processor is not chosen yet (task 1004906, N2).
22
+ // Until someone has confirmed in writing what it screens, a screening the
23
+ // processor reports is NOT accepted as the basis for paying: only a screening a
24
+ // person with payout.approve recorded counts. PROCESSOR_SCREENING_CONFIRMED is the
25
+ // one switch N2 flips, with a reference to that written confirmation.
26
+ //
27
+ // Pure first (decide), then the db half (pool injectable).
28
+
29
+ const PROCESSOR_SCREENING_CONFIRMED = null; // e.g. { processor: 'x', scope: 'OFAC+EU+UN', confirmed_in: 'ADR NNNN' }
30
+
31
+ const AGE_TIERS = Object.freeze(['under_13', '13_15', '16_17', 'adult']);
32
+ const AGE_BASES = Object.freeze(['self_declared', 'verified_by_processor', 'verified_by_person']);
33
+ const SCREEN_RESULTS = Object.freeze(['clear', 'match', 'error']);
34
+ const FREEZE_REASONS = Object.freeze(['sanctions_match', 'sanctions_unscreened', 'age_no_money', 'age_needs_guardian', 'age_unknown', 'manual']);
35
+ const APPEAL_ROUTE = 'POST /api/bongos/me/payout-freezes/:id/appeal';
36
+ const MAX_SCREEN_DAYS = 365;
37
+
38
+ // The words a frozen person reads. Plain, never accusing, always naming the way out.
39
+ const NOTICES = Object.freeze({
40
+ sanctions_match: 'A payout to you is on hold because a required sanctions check flagged a possible match. This is often a false match. You can appeal and we will review it with you.',
41
+ sanctions_unscreened: 'A payout to you is waiting for a required sanctions check before it can be paid. Nothing is lost; it is paid once the check is done. You can appeal if it is taking too long.',
42
+ age_no_money: 'A payout to you cannot be paid in money because of the age recorded on your account. If the age is wrong, you can appeal and have it corrected.',
43
+ age_needs_guardian: 'A payout to you needs a parent or guardian to co-sign it and an account they control before it can be paid. You can appeal if this is wrong.',
44
+ age_unknown: 'A payout to you is waiting because your account has no age recorded yet. Record your age to continue, or appeal if you think this is wrong.',
45
+ });
46
+
47
+ const latest = (rows) => (rows && rows.length ? rows[rows.length - 1] : null);
48
+
49
+ function screeningCounts(row) {
50
+ if (!row) return false;
51
+ if (row.screened_by.startsWith('person:')) return true;
52
+ return Boolean(PROCESSOR_SCREENING_CONFIRMED);
53
+ }
54
+
55
+ // Is this recipient payable in money right now? facts: their rows, oldest first.
56
+ // Returns { ok: true, pay_to } or { ok: false, reason, freeze_id? }.
57
+ function decide({ ageRecords = [], guardians = [], screenings = [], freezes = [], freezeEvents = [], now = new Date() }) {
58
+ const lifted = new Set(freezeEvents.filter((e) => e.kind === 'lifted').map((e) => String(e.freeze_id)));
59
+ const open = freezes.filter((f) => !lifted.has(String(f.id)));
60
+ if (open.length) return { ok: false, reason: 'frozen', freeze_id: String(open[open.length - 1].id), frozen_for: open[open.length - 1].reason };
61
+
62
+ const age = latest(ageRecords);
63
+ if (!age) return { ok: false, reason: 'age_unknown' };
64
+ if (age.tier === 'under_13' || age.tier === '13_15') return { ok: false, reason: 'age_no_money' };
65
+ let payTo = 'self';
66
+ if (age.tier === '16_17') {
67
+ const g = latest(guardians);
68
+ if (!g || !g.active || !g.guardian_account) return { ok: false, reason: 'age_needs_guardian' };
69
+ payTo = { guardian_id: String(g.guardian_id), guardian_account: g.guardian_account };
70
+ }
71
+
72
+ const counted = screenings.filter(screeningCounts);
73
+ const s = latest(counted);
74
+ if (s && s.result === 'match') return { ok: false, reason: 'sanctions_match' };
75
+ if (!s || s.result !== 'clear' || !s.valid_until || new Date(s.valid_until) < now) return { ok: false, reason: 'sanctions_unscreened' };
76
+
77
+ return { ok: true, pay_to: payTo, needs_guardian_cosign: payTo !== 'self' };
78
+ }
79
+
80
+
81
+ // ── db half ────────────────────────────────────────────────────────────────
82
+
83
+ function defaultPool() { return require('./pool').pool; }
84
+
85
+ const ids = (rows) => rows.map((r) => {
86
+ const o = { ...r };
87
+ for (const k of ['id', 'builder_id', 'guardian_id', 'recorded_by', 'frozen_by', 'freeze_id', 'actor_id']) if (o[k] != null) o[k] = String(o[k]);
88
+ if (o.owed_cents != null) o.owed_cents = Number(o.owed_cents);
89
+ return o;
90
+ });
91
+
92
+ // Everything the gate reads about one builder, oldest first per table.
93
+ // Everything the gate reads about a SET of builders: five queries however many
94
+ // recipients a run has (one per table, then the holds' events), never one round
95
+ // of reads per person. Returns Map<builderId, facts>, each table oldest first.
96
+ async function readFactsMany(builderIds, { pool = defaultPool() } = {}) {
97
+ const want = [...new Set(builderIds.map(String))];
98
+ const out = new Map(want.map((id) => [id, { ageRecords: [], guardians: [], screenings: [], freezes: [], freezeEvents: [] }]));
99
+ if (!want.length) return out;
100
+ // One recipient: every row is theirs. Many: rows carry builder_id.
101
+ const owner = (r) => (want.length === 1 ? want[0] : String(r.builder_id));
102
+ const q = (sql) => pool.query(sql, [want]).then((r) => ids(r.rows));
103
+ const [ageRecords, guardians, screenings, freezes] = await Promise.all([
104
+ q('SELECT id, builder_id, tier, basis, recorded_by, recorded_at FROM payout_age_records WHERE builder_id = ANY($1::bigint[]) ORDER BY id'),
105
+ q('SELECT id, builder_id, guardian_id, guardian_account, active, recorded_by, recorded_at FROM payout_guardians WHERE builder_id = ANY($1::bigint[]) ORDER BY id'),
106
+ q('SELECT id, builder_id, result, screened_by, reference, valid_until, recorded_by, screened_at FROM payout_screenings WHERE builder_id = ANY($1::bigint[]) ORDER BY id'),
107
+ q('SELECT id, builder_id, reason, notice_text, appeal_route, owed_cents, frozen_by, frozen_at FROM payout_freezes WHERE builder_id = ANY($1::bigint[]) ORDER BY id'),
108
+ ]);
109
+ for (const [key, rows] of [['ageRecords', ageRecords], ['guardians', guardians], ['screenings', screenings], ['freezes', freezes]]) {
110
+ for (const r of rows) { const b = out.get(owner(r)); if (b) b[key].push(r); }
111
+ }
112
+ if (freezes.length) {
113
+ const freezeOwner = new Map(freezes.map((fr) => [String(fr.id), owner(fr)]));
114
+ const events = ids((await pool.query('SELECT id, freeze_id, kind, actor_id, statement, created_at FROM payout_freeze_events WHERE freeze_id = ANY($1::bigint[]) ORDER BY id', [freezes.map((fr) => fr.id)])).rows);
115
+ for (const e of events) { const b = out.get(freezeOwner.get(String(e.freeze_id))); if (b) b.freezeEvents.push(e); }
116
+ }
117
+ return out;
118
+ }
119
+
120
+ async function readFacts(builderId, opts = {}) {
121
+ return (await readFactsMany([builderId], opts)).get(String(builderId));
122
+ }
123
+
124
+ async function gateRecipient(builderId, { now = new Date(), pool = defaultPool() } = {}) {
125
+ const facts = await readFacts(builderId, { pool });
126
+ return { ...decide({ ...facts, now }), facts };
127
+ }
128
+
129
+ // Money is owed and the gate said no: file the freeze that tells them (or reuse
130
+ // the open one for the same reason). Never refuse owed pay without this.
131
+ async function holdOwedPay(builderId, reason, opts = {}) {
132
+ if (!NOTICES[reason]) throw new Error(`holdOwedPay: no notice for ${reason}`);
133
+ const pool = opts.pool || defaultPool();
134
+ // Two money routes refusing the same person at once must not file two holds.
135
+ // A transaction-scoped advisory lock on the builder serialises the
136
+ // check-then-insert; under READ COMMITTED each statement after the lock sees
137
+ // the other's committed row. A caller that hands in a bare client (a test, or
138
+ // a caller already inside its own transaction) runs the two statements as is.
139
+ // A Pool has connect() and no release(); a checked-out client has both, and
140
+ // must not be connected again.
141
+ if (typeof pool.connect === 'function' && typeof pool.release !== 'function') {
142
+ const client = await pool.connect();
143
+ try {
144
+ await client.query('BEGIN');
145
+ await client.query('SELECT pg_advisory_xact_lock($1::int, hashtext($2::text))', [HOLD_LOCK_SPACE, String(builderId)]);
146
+ const out = await fileOrReuseHold(client, builderId, reason, opts);
147
+ await client.query('COMMIT');
148
+ return out;
149
+ } catch (err) {
150
+ await client.query('ROLLBACK').catch(() => {});
151
+ throw err;
152
+ } finally {
153
+ client.release();
154
+ }
155
+ }
156
+ return fileOrReuseHold(pool, builderId, reason, opts);
157
+ }
158
+
159
+ const HOLD_LOCK_SPACE = 1004914;
160
+
161
+ async function fileOrReuseHold(pool, builderId, reason, { owedCents = null, frozenBy = null } = {}) {
162
+ const { rows } = await pool.query(
163
+ `INSERT INTO payout_freezes (builder_id, reason, notice_text, appeal_route, owed_cents, frozen_by)
164
+ SELECT $1, $2, $3, $4, $5, $6
165
+ WHERE NOT EXISTS (
166
+ SELECT 1 FROM payout_freezes f
167
+ WHERE f.builder_id = $1 AND f.reason = $2
168
+ AND NOT EXISTS (SELECT 1 FROM payout_freeze_events e WHERE e.freeze_id = f.id AND e.kind = 'lifted'))
169
+ RETURNING id`,
170
+ [builderId, reason, NOTICES[reason], APPEAL_ROUTE, owedCents, frozenBy],
171
+ );
172
+ if (rows[0]) return { freeze_id: String(rows[0].id), filed: true };
173
+ const open = await pool.query(
174
+ `SELECT f.id FROM payout_freezes f WHERE f.builder_id = $1 AND f.reason = $2
175
+ AND NOT EXISTS (SELECT 1 FROM payout_freeze_events e WHERE e.freeze_id = f.id AND e.kind = 'lifted')
176
+ ORDER BY f.id DESC LIMIT 1`, [builderId, reason]);
177
+ return { freeze_id: open.rows[0] ? String(open.rows[0].id) : null, filed: false };
178
+ }
179
+
180
+ // The one call a money route makes per recipient: gate, and on a refusal of owed
181
+ // money, hold it WITH a notice. Returns the gate verdict plus freeze_id.
182
+ async function gateOwedPay(builderId, { owedCents = null, frozenBy = null, now = new Date(), pool = defaultPool() } = {}) {
183
+ const v = await gateRecipient(builderId, { now, pool });
184
+ if (v.ok) return { ok: true, pay_to: v.pay_to, needs_guardian_cosign: v.needs_guardian_cosign };
185
+ if (v.reason === 'frozen') return { ok: false, reason: 'frozen', freeze_id: v.freeze_id, notice: NOTICES[v.frozen_for] || null };
186
+ const hold = await holdOwedPay(builderId, v.reason, { owedCents, frozenBy, pool });
187
+ return { ok: false, reason: v.reason, freeze_id: hold.freeze_id, notice: NOTICES[v.reason] };
188
+ }
189
+
190
+ // gateOwedPay for a whole run: Map<recipientId, cents owed> in, Map<recipientId,
191
+ // verdict> out, from one readFactsMany. Holds are filed only for the recipients
192
+ // refused, which is the rare path.
193
+ async function gateOwedPayMany(owed, { frozenBy = null, now = new Date(), pool = defaultPool() } = {}) {
194
+ const facts = await readFactsMany([...owed.keys()], { pool });
195
+ const out = new Map();
196
+ for (const [rid, cents] of owed) {
197
+ const v = decide({ ...facts.get(String(rid)), now });
198
+ if (v.ok) { out.set(rid, { ok: true, pay_to: v.pay_to, needs_guardian_cosign: v.needs_guardian_cosign }); continue; }
199
+ if (v.reason === 'frozen') { out.set(rid, { ok: false, reason: 'frozen', freeze_id: v.freeze_id, notice: NOTICES[v.frozen_for] || null }); continue; }
200
+ const hold = await holdOwedPay(rid, v.reason, { owedCents: cents, frozenBy, pool });
201
+ out.set(rid, { ok: false, reason: v.reason, freeze_id: hold.freeze_id, notice: NOTICES[v.reason] });
202
+ }
203
+ return out;
204
+ }
205
+
206
+ // Which of these recipients has their guardian's co-signature on this run line:
207
+ // one query for the run.
208
+ async function cosignedInRun({ ruleId, runKey, builderIds }, { pool = defaultPool() } = {}) {
209
+ if (!builderIds.length) return new Set();
210
+ const { rows } = await pool.query(
211
+ 'SELECT DISTINCT builder_id FROM payout_guardian_cosigns WHERE rule_id = $1 AND run_key = $2 AND builder_id = ANY($3::bigint[])',
212
+ [ruleId, runKey, builderIds.map(String)],
213
+ );
214
+ return new Set(rows.map((r) => String(r.builder_id)));
215
+ }
216
+
217
+ async function insertRow(table, cols, vals, { pool = defaultPool() } = {}) {
218
+ const ph = vals.map((_, i) => `$${i + 1}`).join(', ');
219
+ const { rows } = await pool.query(`INSERT INTO ${table} (${cols.join(', ')}) VALUES (${ph}) RETURNING *`, vals);
220
+ return ids(rows)[0];
221
+ }
222
+
223
+ const recordAge = ({ builderId, tier, basis, recordedBy }, o) => insertRow('payout_age_records', ['builder_id', 'tier', 'basis', 'recorded_by'], [builderId, tier, basis, recordedBy], o);
224
+ const recordGuardian = ({ builderId, guardianId, guardianAccount, active, recordedBy }, o) => insertRow('payout_guardians', ['builder_id', 'guardian_id', 'guardian_account', 'active', 'recorded_by'], [builderId, guardianId, guardianAccount, active, recordedBy], o);
225
+ const recordScreening = ({ builderId, result, screenedBy, reference, validUntil, recordedBy }, o) => insertRow('payout_screenings', ['builder_id', 'result', 'screened_by', 'reference', 'valid_until', 'recorded_by'], [builderId, result, screenedBy, reference, validUntil, recordedBy], o);
226
+ const freezeManually = ({ builderId, noticeText, owedCents, frozenBy }, o) => insertRow('payout_freezes', ['builder_id', 'reason', 'notice_text', 'appeal_route', 'owed_cents', 'frozen_by'], [builderId, 'manual', noticeText, APPEAL_ROUTE, owedCents, frozenBy], o);
227
+ const addFreezeEvent = ({ freezeId, kind, actorId, statement }, o) => insertRow('payout_freeze_events', ['freeze_id', 'kind', 'actor_id', 'statement'], [freezeId, kind, actorId, statement], o);
228
+
229
+ async function readFreeze(freezeId, { pool = defaultPool() } = {}) {
230
+ const f = ids((await pool.query('SELECT id, builder_id, reason, notice_text, appeal_route, owed_cents, frozen_by, frozen_at FROM payout_freezes WHERE id = $1', [freezeId])).rows)[0];
231
+ if (!f) return null;
232
+ const events = ids((await pool.query('SELECT id, freeze_id, kind, actor_id, statement, created_at FROM payout_freeze_events WHERE freeze_id = $1 ORDER BY id', [freezeId])).rows);
233
+ return { ...f, events, lifted: events.some((e) => e.kind === 'lifted') };
234
+ }
235
+
236
+ const recordCosign = ({ builderId, guardianId, approvalId, ruleId, runKey, sessionId }, o) => insertRow('payout_guardian_cosigns', ['builder_id', 'guardian_id', 'approval_id', 'rule_id', 'run_key', 'session_id', 'session_source'], [builderId, guardianId, approvalId, ruleId, runKey, sessionId, 'web'], o);
237
+
238
+ // Co-sign as a guardian: find whose payout this is from the payout itself (a
239
+ // one-off's recipient, or the run's noticed lines), keep only the builders whose
240
+ // CURRENT guardian is guardianId, and record one co-signature each. Returns the
241
+ // rows written ([] when the payout is not to any of their wards). The insert
242
+ // trigger checks the guardianship again, so this can only narrow.
243
+ async function cosignAsGuardian({ guardianId, sessionId, approvalId = null, ruleId = null, runKey = null }, { pool = defaultPool() } = {}) {
244
+ let recipients = [];
245
+ if (approvalId) {
246
+ const { rows } = await pool.query("SELECT recipient_id FROM payout_approvals WHERE id = $1 AND kind = 'payout_approved'", [approvalId]);
247
+ recipients = rows.map((r) => String(r.recipient_id));
248
+ } else {
249
+ const { rows } = await pool.query("SELECT terms FROM payout_approvals WHERE rule_id = $1 AND run_key = $2 AND kind = 'run_noticed'", [ruleId, runKey]);
250
+ const lines = rows[0] && rows[0].terms && Array.isArray(rows[0].terms.payouts) ? rows[0].terms.payouts : [];
251
+ recipients = [...new Set(lines.map((p) => String(p.recipient_id)))];
252
+ }
253
+ if (!recipients.length) return [];
254
+ const { rows: wards } = await pool.query(
255
+ `SELECT g.builder_id FROM payout_guardians g
256
+ WHERE g.builder_id = ANY($1::bigint[])
257
+ AND g.id = (SELECT max(id) FROM payout_guardians x WHERE x.builder_id = g.builder_id)
258
+ AND g.active AND g.guardian_id = $2`,
259
+ [recipients, guardianId],
260
+ );
261
+ const out = [];
262
+ for (const w of wards) {
263
+ out.push(await recordCosign({ builderId: String(w.builder_id), guardianId, approvalId, ruleId, runKey, sessionId }, { pool }));
264
+ }
265
+ return out;
266
+ }
267
+
268
+ async function hasCosign({ builderId, approvalId = null, ruleId = null, runKey = null }, { pool = defaultPool() } = {}) {
269
+ const { rows } = approvalId
270
+ ? await pool.query('SELECT 1 FROM payout_guardian_cosigns WHERE builder_id = $1 AND approval_id = $2 LIMIT 1', [builderId, approvalId])
271
+ : await pool.query('SELECT 1 FROM payout_guardian_cosigns WHERE builder_id = $1 AND rule_id = $2 AND run_key = $3 LIMIT 1', [builderId, ruleId, runKey]);
272
+ return rows.length > 0;
273
+ }
274
+
275
+ // What an approver sees about a recipient.
276
+ function present(v) {
277
+ return {
278
+ payable: Boolean(v.ok),
279
+ reason: v.ok ? null : v.reason,
280
+ notice: v.ok ? null : (NOTICES[v.reason === 'frozen' ? v.frozen_for : v.reason] || null),
281
+ freeze_id: v.freeze_id || null,
282
+ pay_to: v.ok ? v.pay_to : null,
283
+ needs_guardian_cosign: Boolean(v.needs_guardian_cosign),
284
+ };
285
+ }
286
+
287
+ // What the person sees about themselves: every hold, its words, what happened since.
288
+ function presentOwn(v) {
289
+ const f = v.facts || {};
290
+ const age = latest(f.ageRecords || []);
291
+ const g = latest(f.guardians || []);
292
+ const s = latest((f.screenings || []).filter(screeningCounts));
293
+ const events = f.freezeEvents || [];
294
+ return {
295
+ payable: Boolean(v.ok),
296
+ reason: v.ok ? null : v.reason,
297
+ age_tier: age ? age.tier : null,
298
+ guardian: g ? { guardian_id: g.guardian_id, active: g.active } : null,
299
+ screening: s ? { result: s.result, valid_until: s.valid_until || null } : null,
300
+ holds: (f.freezes || []).map((fr) => {
301
+ const mine = events.filter((e) => String(e.freeze_id) === String(fr.id));
302
+ return {
303
+ id: fr.id,
304
+ reason: fr.reason,
305
+ notice: fr.notice_text,
306
+ owed_cents: fr.owed_cents ?? null,
307
+ frozen_at: fr.frozen_at,
308
+ lifted: mine.some((e) => e.kind === 'lifted'),
309
+ events: mine.map((e) => ({ kind: e.kind, statement: e.statement, at: e.created_at })),
310
+ appeal: APPEAL_ROUTE.replace(':id', fr.id),
311
+ };
312
+ }),
313
+ };
314
+ }
315
+
316
+ // How many holds on this builder's pay are still open, for the /me needs list:
317
+ // { count, first_id }. One indexed read on the session hot path.
318
+ async function openHoldsFor(builderId, { pool = defaultPool() } = {}) {
319
+ const { rows } = await pool.query(
320
+ `SELECT count(*)::int AS count, min(f.id) AS first_id FROM payout_freezes f
321
+ WHERE f.builder_id = $1
322
+ AND NOT EXISTS (SELECT 1 FROM payout_freeze_events e WHERE e.freeze_id = f.id AND e.kind = 'lifted')`,
323
+ [builderId],
324
+ );
325
+ const r = rows[0] || {};
326
+ return { count: Number(r.count) || 0, first_id: r.first_id == null ? null : String(r.first_id) };
327
+ }
328
+
329
+ // One object, required whole by its callers (routes/payout-gates.js,
330
+ // routes/payout-approvals.js, payout-approvals.js, routes/me.js), each of which
331
+ // takes it as an injectable dependency so tests can stand in for the database.
332
+ const api = {
333
+ PROCESSOR_SCREENING_CONFIRMED,
334
+ AGE_TIERS,
335
+ AGE_BASES,
336
+ SCREEN_RESULTS,
337
+ FREEZE_REASONS,
338
+ NOTICES,
339
+ APPEAL_ROUTE,
340
+ MAX_SCREEN_DAYS,
341
+ screeningCounts,
342
+ decide,
343
+ present,
344
+ presentOwn,
345
+ readFacts,
346
+ readFactsMany,
347
+ gateOwedPayMany,
348
+ cosignedInRun,
349
+ gateRecipient,
350
+ holdOwedPay,
351
+ gateOwedPay,
352
+ recordAge,
353
+ recordGuardian,
354
+ recordScreening,
355
+ recordCosign,
356
+ cosignAsGuardian,
357
+ hasCosign,
358
+ freezeManually,
359
+ addFreezeEvent,
360
+ readFreeze,
361
+ openHoldsFor,
362
+ };
363
+
364
+ module.exports = api;
@@ -87,14 +87,13 @@ const SUBMIT_PATHS = [SUBMIT_PATH, LEGACY_SUBMIT_PATH];
87
87
  // instance without the raise module the path is simply a 404.
88
88
  const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__prelaunch$/, /^\/__stealth$/, /^\/raise(\/|$)/];
89
89
 
90
- // PRELAUNCH_PASSWORD is primary; STEALTH_PASSWORD is the deprecated alias. This
91
- // is the ONE read site for either, which is what makes honouring both a single
92
- // expression rather than a migration — and why the rename could be taken at all
93
- // without an outage window on a live instance already running the old name.
94
- function password() {
95
- const p = process.env.PRELAUNCH_PASSWORD || process.env.STEALTH_PASSWORD;
96
- return typeof p === 'string' && p.length ? p : null;
97
- }
90
+ // PRELAUNCH_PASSWORD is primary; STEALTH_PASSWORD is the deprecated alias. The ONE
91
+ // read site for either is ./prelaunch-switch.js (moved there by task 1004998 so the
92
+ // module doorway can ask "is pre-launch on?" without this file joining the kernel),
93
+ // which is what makes honouring both a single expression rather than a migration —
94
+ // and why the rename could be taken at all without an outage window on a live
95
+ // instance already running the old name.
96
+ const { password, enabled } = require('./prelaunch-switch');
98
97
 
99
98
  // Named once at mount, never per request: a warning on every gated request would
100
99
  // be a log flood on exactly the instance that is hardest to look at. Dropping the
@@ -109,7 +108,6 @@ function warnIfLegacyEnv(log = console.warn) {
109
108
  return true;
110
109
  }
111
110
 
112
- function enabled() { return password() !== null; }
113
111
 
114
112
  // The token a valid cookie must carry. Keyed by the password, so the password
115
113
  // itself never leaves the server and a rotation invalidates every cookie.
@@ -0,0 +1,20 @@
1
+ // src/bongos/prelaunch-switch.js — is this instance in pre-launch (stealth)? (task 1004998)
2
+ //
3
+ // WHY. The pre-launch password gate (./prelaunch-gate.js) decides whether a
4
+ // stranger may see the instance at all; other doors need to ask only whether it
5
+ // is ON — the provisioning module refuses public project repos while it is
6
+ // (through src/module-api.js prelaunchActive). The gate itself cannot be a kernel
7
+ // file (it names the `raise` module's exempt path), so the one question both
8
+ // share lives here, imports nothing, and the gate reads its password from it.
9
+ // PRELAUNCH_PASSWORD wins; STEALTH_PASSWORD is the deprecated alias still set on
10
+ // live (see the gate's header).
11
+ 'use strict';
12
+
13
+ function password() {
14
+ const p = process.env.PRELAUNCH_PASSWORD || process.env.STEALTH_PASSWORD;
15
+ return typeof p === 'string' && p.length ? p : null;
16
+ }
17
+
18
+ function enabled() { return password() !== null; }
19
+
20
+ module.exports = { password, enabled };
@@ -301,6 +301,9 @@ const GOVERNMENT_PINS = {
301
301
  // task 1004523: the governor's docket — every matter awaiting a governor. The
302
302
  // coarse gate is the room's; each source inside is scoped to its own atom.
303
303
  'GET /government/docket': 'perm:board.vote.cast',
304
+ // task 1004999: open blockers under the people they wait on, and the viewer's own.
305
+ 'GET /government/blockers/people': 'perm:board.vote.cast',
306
+ 'GET /government/blockers/for-me': 'perm:board.vote.cast',
304
307
  // task 1004529: who decides what — a change to the decision-rules table is an
305
308
  // amendment by another door (same atom as proposing one), and a building's
306
309
  // matter is put to its sitting by the same atom. Only a passed amendment