@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,308 @@
1
+ // tests/market_watch_data_db.mjs — the Market Watch tables, seed and read queries against a
2
+ // REAL Postgres (task 1004936, goal 1000137).
3
+ //
4
+ // What only a real engine can say: the migration applies, and applies again as a no-op;
5
+ // the hub seed loads 60 items and loads again inserting nothing; the CHECKs and partial
6
+ // unique indexes refuse the rows they exist to refuse (an unsourced entry, a done item with
7
+ // no cost, a skipped run with no reason, a second active rules version, a second seed run);
8
+ // and the read queries return the levels, streaks, run numbers, sparklines, spend and
9
+ // resume set the screens and the weekly run need.
10
+ //
11
+ // The module is default:false, so CI's migrate.sh does not apply its migrations; this file
12
+ // applies market_watch_001 itself (twice). Its rows live under a project origin no real
13
+ // instance has and are deleted before and after. Real-DB integration test: self-skips
14
+ // without a reachable Postgres; in the INTEGRATION set (scripts/bongos/run-unit-tests.js)
15
+ // so the DB-free lane never runs it. The DB-free half is tests/market_watch_data.mjs.
16
+ //
17
+ // Run: DATABASE_URL=... node tests/market_watch_data_db.mjs
18
+
19
+ import { createRequire } from 'node:module';
20
+ import { readFileSync } from 'node:fs';
21
+ import { strict as assert } from 'node:assert';
22
+ import { insertTestBuilders } from './helpers.mjs';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const { Pool } = require('pg');
26
+ const seed = require('../modules/market-watch/seed.js');
27
+ const db = require('../modules/market-watch/db.js');
28
+
29
+ const MIGRATION = readFileSync(new URL('../modules/market-watch/migrations/market_watch_001_tables.sql', import.meta.url), 'utf8');
30
+ const MIGRATION_002 = readFileSync(new URL('../modules/market-watch/migrations/market_watch_002_run.sql', import.meta.url), 'utf8');
31
+ const run = require('../modules/market-watch/run.js');
32
+ const store = require('../modules/market-watch/store.js');
33
+
34
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
35
+ try {
36
+ await pool.query('SELECT 1');
37
+ } catch (e) {
38
+ console.log(`market_watch_data_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
39
+ await pool.end().catch(() => {});
40
+ process.exit(0);
41
+ }
42
+
43
+ const P = 'https://t1004936-hub.example';
44
+ const OTHER = 'https://t1004936-other.example';
45
+ const ALICE = 91004936, BOB = 91004937;
46
+ const q = (sql, params) => pool.query(sql, params);
47
+
48
+ async function cleanup() {
49
+ for (const project of [P, OTHER]) {
50
+ await q(`DELETE FROM market_watch_proposals WHERE project = $1`, [project]).catch(() => {});
51
+ await q(`DELETE FROM market_watch_ask_answers WHERE ask_id IN (SELECT id FROM market_watch_weekly_asks WHERE project = $1)`, [project]).catch(() => {});
52
+ await q(`DELETE FROM market_watch_weekly_asks WHERE project = $1`, [project]).catch(() => {});
53
+ await q(`DELETE FROM market_watch_score_snapshots WHERE item_id IN (SELECT id FROM market_watch_items WHERE project = $1)`, [project]).catch(() => {});
54
+ await q(`DELETE FROM market_watch_log_entries WHERE item_id IN (SELECT id FROM market_watch_items WHERE project = $1)`, [project]).catch(() => {});
55
+ await q(`DELETE FROM market_watch_run_items WHERE run_id IN (SELECT id FROM market_watch_runs WHERE project = $1)`, [project]).catch(() => {});
56
+ await q(`DELETE FROM market_watch_runs WHERE project = $1`, [project]).catch(() => {});
57
+ await q(`DELETE FROM market_watch_items WHERE project = $1`, [project]).catch(() => {});
58
+ await q(`DELETE FROM market_watch_rule_versions WHERE project = $1`, [project]).catch(() => {});
59
+ await q(`DELETE FROM market_watch_settings WHERE project = $1`, [project]).catch(() => {});
60
+ }
61
+ await q(`DELETE FROM builders WHERE id = ANY($1::bigint[])`, [[ALICE, BOB]]).catch(() => {});
62
+ }
63
+
64
+ // A statement that must be refused by the schema. Each runs in its own SAVEPOINT-free
65
+ // autocommit statement, so a refusal leaves nothing behind.
66
+ async function refused(sql, params, why) {
67
+ await assert.rejects(q(sql, params), /violates|invalid input/, why);
68
+ }
69
+
70
+ let failed = 0;
71
+ async function check(name, fn) {
72
+ try { await fn(); console.log(`ok - ${name}`); } catch (e) { failed += 1; console.log(`not ok - ${name}\n ${e && e.stack}`); }
73
+ }
74
+
75
+ try {
76
+ await check('the migration applies, and applies again as a no-op', async () => {
77
+ await q(MIGRATION);
78
+ await q(MIGRATION);
79
+ // market_watch_002 (task 1004937): the run's key columns, one-open-run index and the
80
+ // 'run' proposal origin. A new migration here joins this file (CI never applies it).
81
+ await q(MIGRATION_002);
82
+ await q(MIGRATION_002);
83
+ const { rows } = await q(`SELECT count(*)::int AS n FROM information_schema.tables WHERE table_name LIKE 'market\\_watch\\_%'`);
84
+ assert.ok(rows[0].n >= 10, `tables: ${rows[0].n}`);
85
+ });
86
+ await cleanup();
87
+ await insertTestBuilders(pool, [{ id: ALICE, login: 't1004936-alice' }, { id: BOB, login: 't1004936-bob' }]);
88
+
89
+ await check('the seed loads off the hub never, on the hub once, and a second load inserts nothing', async () => {
90
+ assert.equal((await seed.seedHub({ pool, isHub: false, project: OTHER })).skipped, 'not_hub');
91
+ const first = await seed.seedHub({ pool, isHub: true, project: P });
92
+ assert.equal(first.seeded, true);
93
+ assert.equal(first.inserted.items, 60);
94
+ assert.equal(first.inserted.scores, 360);
95
+ assert.equal(first.inserted.runs, 1);
96
+ assert.equal(first.inserted.rules, 1);
97
+ assert.ok(first.inserted.entries > 60);
98
+ const again = await seed.seedHub({ pool, isHub: true, project: P });
99
+ assert.deepEqual(again.inserted, { rules: 0, runs: 0, items: 0, scores: 0, entries: 0 });
100
+ const { rows: [n] } = await q(`SELECT count(*)::int AS items, count(*) FILTER (WHERE kind = 'competitor')::int AS comps
101
+ FROM market_watch_items WHERE project = $1`, [P]);
102
+ assert.deepEqual(n, { items: 60, comps: 36 });
103
+ assert.equal((await db.listItems(pool, { project: OTHER })).length, 0, 'nothing reached the other project');
104
+ });
105
+
106
+ await check('a removed item stays removed when the seed runs again', async () => {
107
+ await q(`UPDATE market_watch_items SET status = 'removed' WHERE project = $1 AND name = 'Sweep'`, [P]);
108
+ await seed.seedHub({ pool, isHub: true, project: P });
109
+ const items = await db.listItems(pool, { project: P });
110
+ assert.equal(items.length, 59);
111
+ assert.ok(!items.some((i) => i.name === 'Sweep'));
112
+ assert.equal((await db.listItems(pool, { project: P, includeRemoved: true })).length, 60);
113
+ });
114
+
115
+ const { rows: [{ id: seedRun }] } = await q(`SELECT id FROM market_watch_runs WHERE project = $1 AND trigger = 'seed'`, [P]);
116
+ const { rows: [{ id: rule1 }] } = await q(`SELECT id FROM market_watch_rule_versions WHERE project = $1 AND version = 1`, [P]);
117
+ const { rows: [copilot] } = await q(`SELECT id FROM market_watch_items WHERE project = $1 AND name = 'GitHub Copilot coding agent'`, [P]);
118
+
119
+ await check('seeded items read at level 1, streak 1, with their overall score and case file', async () => {
120
+ const items = await db.listItems(pool, { project: P, kind: 'competitor' });
121
+ assert.equal(items.length, 35);
122
+ const c = items.find((i) => i.id === Number(copilot.id));
123
+ assert.equal(c.level, 1);
124
+ assert.equal(c.streak, 1);
125
+ assert.equal(typeof c.overall, 'number');
126
+ const file = await db.caseFile(pool, { project: P, itemId: copilot.id });
127
+ assert.ok(file.length >= 1);
128
+ for (const e of file) { assert.equal(e.run, 1); assert.match(e.source_url, /^https?:\/\//); }
129
+ assert.equal(await db.getItem(pool, { project: OTHER, id: copilot.id }), null, 'another project cannot read it');
130
+ });
131
+
132
+ // A skipped run, then a real one that finds something on Copilot and re-scores it.
133
+ const { rows: [{ id: skipped }] } = await q(
134
+ `INSERT INTO market_watch_runs (project, status, reason, trigger, created_at) VALUES ($1, 'skipped', 'cap_reached', 'poller', '2026-11-04T09:00:00Z') RETURNING id`, [P]);
135
+ const { rows: [{ id: run2 }] } = await q(
136
+ `INSERT INTO market_watch_runs (project, status, trigger, rule_version_id, started_at, finished_at, created_at)
137
+ VALUES ($1, 'done', 'poller', $2, now(), now(), '2026-11-11T09:00:00Z') RETURNING id`, [P, rule1]);
138
+ await q(`INSERT INTO market_watch_run_items (run_id, item_id, status, cost_usd, finished_at) VALUES ($1, $2, 'done', 0.0950, '2026-11-11T09:05:00Z')`, [run2, copilot.id]);
139
+ await q(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence) VALUES ($1, $2, 'news', 'Agent mode reaches self-hosted Enterprise Server.', 'https://github.blog/x', 3)`, [copilot.id, run2]);
140
+ await q(`INSERT INTO market_watch_score_snapshots (item_id, run_id, dimension, value) VALUES ($1, $2, 'overall', 7), ($1, $2, 'human_gates', 8)`, [copilot.id, run2]);
141
+
142
+ await check('a later run levels the item up, a skipped run takes no number and breaks no streak', async () => {
143
+ const c = await db.getItem(pool, { project: P, id: copilot.id });
144
+ assert.deepEqual([c.level, c.streak, c.overall], [2, 2, 7]);
145
+ const other = (await db.listItems(pool, { project: P })).find((i) => i.name === 'Cursor (Anysphere)');
146
+ assert.deepEqual([other.level, other.streak], [1, 0], 'nothing found on it in the latest run');
147
+ const runs = await db.numberedRuns(pool, { project: P });
148
+ assert.deepEqual(runs.map((r) => [Number(r.id), r.n]), [[Number(seedRun), 1], [Number(skipped), null], [Number(run2), 2]]);
149
+ const file = await db.caseFile(pool, { project: P, itemId: copilot.id });
150
+ assert.equal(file[0].run, 2, 'newest first');
151
+ });
152
+
153
+ await check('the sparklines align on the runs that scored the item', async () => {
154
+ const h = await db.scoreHistory(pool, { project: P, itemId: copilot.id });
155
+ assert.deepEqual(h.runs.map((r) => r.n), [1, 2]);
156
+ assert.equal(h.series.overall[1], 7);
157
+ assert.equal(h.series.human_gates.length, 2);
158
+ assert.equal(h.series.ai_agents_write_code[1], null, 'not scored in run 2: null, never carried forward');
159
+ });
160
+
161
+ await check('a resumed run is offered only the items it has not done', async () => {
162
+ const left = await db.remainingItems(pool, { project: P, runId: run2 });
163
+ assert.equal(left.length, 58, '59 active minus the one done');
164
+ assert.ok(!left.some((i) => i.id === Number(copilot.id)));
165
+ assert.deepEqual(await db.remainingItems(pool, { project: OTHER, runId: run2 }), [], 'another project cannot read the run');
166
+ });
167
+
168
+ await check('settings read the defaults, then the row, with this month spend and the skipped run on the strip', async () => {
169
+ let v = await db.settingsView(pool, { project: P, now: new Date('2026-11-20T00:00:00Z') });
170
+ assert.equal(v.settings.configured, false);
171
+ assert.equal(v.settings.enabled, false);
172
+ assert.equal(v.spentThisMonthUsd, 0.095);
173
+ assert.deepEqual(v.recentRuns.map((r) => [r.status, r.reason]), [['done', null], ['skipped', 'cap_reached']], 'the seed run is not on the strip');
174
+ assert.equal(v.recentRuns[0].items_done, 1);
175
+ assert.equal(v.nextRunDue.toISOString(), '2026-11-18T09:00:00.000Z');
176
+ await q(`INSERT INTO market_watch_settings (project, enabled, monthly_cap_usd, cadence, updated_by) VALUES ($1, true, 25, 'biweekly', $2)`, [P, ALICE]);
177
+ v = await db.settingsView(pool, { project: P, now: new Date('2026-12-02T00:00:00Z') });
178
+ assert.deepEqual([v.settings.enabled, v.settings.monthly_cap_usd, v.settings.cadence], [true, 25, 'biweekly']);
179
+ assert.equal(v.spentThisMonthUsd, 0, 'a new month starts at zero');
180
+ });
181
+
182
+ await check('rules: one active, one proposed next, and the history in order', async () => {
183
+ const { rows: [{ id: v2 }] } = await q(
184
+ `INSERT INTO market_watch_rule_versions (project, version, status, rubric, summary, reason, author_id)
185
+ VALUES ($1, 2, 'proposed', '{}'::jsonb, '+ bounty boards', 'finds Algora and Opire', $2) RETURNING id`, [P, BOB]);
186
+ const r = await db.rules(pool, { project: P });
187
+ assert.deepEqual(r.versions.map((v) => [v.version, v.status]), [[1, 'active'], [2, 'proposed']]);
188
+ assert.equal(r.active.version, 1);
189
+ assert.equal(r.proposed.id, Number(v2));
190
+ assert.equal(r.active.rubric.competitor.length, 5);
191
+ await refused(`INSERT INTO market_watch_rule_versions (project, version, status, reason) VALUES ($1, 3, 'proposed', 'x')`, [P], 'a second proposed version');
192
+ await refused(`INSERT INTO market_watch_rule_versions (project, version, status, reason) VALUES ($1, 3, 'active', 'x')`, [P], 'a second active version');
193
+ await q(`INSERT INTO market_watch_proposals (project, kind, origin, rule_version_id, reason, proposed_by) VALUES ($1, 'rule_change', 'member', $2, 'count bounty boards', $3)`, [P, v2, BOB]);
194
+ });
195
+
196
+ await check('a weekly ask pools taps into tallies and proposals, and the member streak reads', async () => {
197
+ const ins = async (week, closes) => (await q(
198
+ `INSERT INTO market_watch_weekly_asks (project, week_of, question_key, prompt, options, closes_at)
199
+ VALUES ($1, $2, 'rival', 'What counts as a competitor?', '[{"label":"AI IDEs"}]'::jsonb, $3) RETURNING id`, [P, week, closes])).rows[0].id;
200
+ const w1 = await ins('2026-11-02', '2026-11-08T00:00:00Z');
201
+ const w2 = await ins('2026-11-09', '2026-11-15T00:00:00Z');
202
+ const w3 = await ins('2026-11-16', '2099-01-01T00:00:00Z');
203
+ const tap = async (ask, who, choice) => (await q(
204
+ `INSERT INTO market_watch_ask_answers (ask_id, builder_id, choice) VALUES ($1, $2, $3) RETURNING id`, [ask, who, choice])).rows[0].id;
205
+ await tap(w1, ALICE, 'AI IDEs');
206
+ await tap(w2, ALICE, 'AI IDEs');
207
+ const a = await tap(w2, BOB, 'AI IDEs');
208
+ await tap(w2, BOB, 'Bounty boards');
209
+ await refused(`INSERT INTO market_watch_ask_answers (ask_id, builder_id, choice) VALUES ($1, $2, 'AI IDEs')`, [w2, BOB], 'a second tap on the same chip');
210
+ const week = await db.weeklyAsk(pool, { project: P, weekOf: '2026-11-09' });
211
+ assert.deepEqual(week[0].tallies, [{ choice: 'AI IDEs', taps: 2 }, { choice: 'Bounty boards', taps: 1 }]);
212
+ assert.equal(await db.askStreak(pool, { project: P, builderId: ALICE, now: new Date('2026-11-17T00:00:00Z') }), 2, 'w3 is open: not yet a miss');
213
+ assert.equal(await db.askStreak(pool, { project: P, builderId: BOB, now: new Date('2026-11-17T00:00:00Z') }), 1);
214
+ await q(`INSERT INTO market_watch_proposals (project, kind, origin, answer_id, payload, proposed_by) VALUES ($1, 'add_item', 'weekly_ask', $2, '{"name":"Cursor"}'::jsonb, $3)`, [P, a, BOB]);
215
+ await refused(`INSERT INTO market_watch_proposals (project, kind, origin, answer_id, payload) VALUES ($1, 'add_item', 'weekly_ask', $2, '{}'::jsonb)`, [P, a], 'one tap is one proposal');
216
+ const open = await db.proposals(pool, { project: P });
217
+ assert.deepEqual(open.map((p) => p.kind).sort(), ['add_item', 'rule_change']);
218
+ assert.equal(w3 > 0, true);
219
+ });
220
+
221
+ await check('the schema refuses what its rules forbid', async () => {
222
+ await refused(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence) VALUES ($1, $2, 'news', 'no source', '', 2)`, [copilot.id, run2], 'an unsourced entry');
223
+ await refused(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence) VALUES ($1, $2, 'news', 'too sure', 'https://x.example', 4)`, [copilot.id, run2], 'confidence above 3');
224
+ await refused(`INSERT INTO market_watch_log_entries (item_id, run_id, type, takeaway, source_url, confidence) VALUES ($1, $2, 'news', 'Agent mode reaches self-hosted Enterprise Server.', 'https://github.blog/y', 3)`, [copilot.id, run2], 'the same takeaway twice in one run (a resumed item)');
225
+ await refused(`INSERT INTO market_watch_run_items (run_id, item_id, status) VALUES ($1, $2, 'done')`, [run2, (await q(`SELECT id FROM market_watch_items WHERE project = $1 AND name = 'Factory'`, [P])).rows[0].id], 'done with no cost');
226
+ await refused(`INSERT INTO market_watch_run_items (run_id, item_id, status, cost_usd) VALUES ($1, $2, 'done', 0.1)`, [run2, copilot.id], 'the same item twice in a run');
227
+ await refused(`INSERT INTO market_watch_runs (project, status, trigger) VALUES ($1, 'skipped', 'poller')`, [P], 'a skipped run with no reason');
228
+ await refused(`INSERT INTO market_watch_runs (project, status, trigger) VALUES ($1, 'done', 'seed')`, [P], 'a second seed run');
229
+ await refused(`INSERT INTO market_watch_score_snapshots (item_id, run_id, dimension, value) VALUES ($1, $2, 'overall', 11)`, [copilot.id, skipped], 'a score above 10');
230
+ await refused(`INSERT INTO market_watch_proposals (project, kind, origin) VALUES ($1, 'rescore', 'member')`, [P], 'a rescore that names no item');
231
+ await refused(`INSERT INTO market_watch_settings (project, cadence) VALUES ($1, 'daily')`, [OTHER], 'an unknown cadence');
232
+ });
233
+
234
+ // The weekly run (task 1004937) through the REAL store on the seeded project: a fake
235
+ // Messages API client (no network, no key spent) answers every item with two sourced
236
+ // entries, two scores and one candidate.
237
+ await check('the weekly run on seed data writes sourced entries, score snapshots, one proposal, and the cost', async () => {
238
+ const NEWS = 'https://news.example.com/launch';
239
+ const answer = {
240
+ stop_reason: 'end_turn',
241
+ content: [
242
+ { type: 'web_search_tool_result', tool_use_id: 's1', content: [{ type: 'web_search_result', url: NEWS }] },
243
+ { type: 'text', text: JSON.stringify({
244
+ entries: [
245
+ { type: 'news', takeaway: 'Shipped a self-hosted agent.', source_url: NEWS, source_label: 'news', confidence: 'high', dimension: null },
246
+ { type: 'news', takeaway: 'Unsourced.', source_url: 'https://invented.example/x', source_label: '', confidence: 'high', dimension: null },
247
+ ],
248
+ scores: [{ dimension: 'overall', value: 6, reason: 'Moved closer.' }],
249
+ candidates: [{ name: 'Newco Agents', url: 'https://newco.example.com', kind: 'competitor', why: 'Named in the launch.' }],
250
+ }) },
251
+ ],
252
+ usage: { input_tokens: 20000, output_tokens: 1500, server_tool_use: { web_search_requests: 2 } },
253
+ };
254
+ const keys = [];
255
+ const mirrored = [];
256
+ const d = {
257
+ project: P,
258
+ store: store.createStore(pool),
259
+ now: () => Date.parse('2027-01-10T10:00:00Z'), // well past run2, in a month with no spend
260
+ readKey: async () => 'sk-ant-test-not-a-real-key-WXYZ',
261
+ createClient: (k) => { keys.push(k); return { messages: { create: async () => answer } }; },
262
+ economy: { logCost: async (row) => { mirrored.push(row); } },
263
+ warn: () => {},
264
+ };
265
+ const r = await run.runDue(d);
266
+ assert.equal(r.outcome, 'done', JSON.stringify(r));
267
+ assert.deepEqual(keys, ['sk-ant-test-not-a-real-key-WXYZ']);
268
+ const { rows: [runRow] } = await q(`SELECT status, reason, claimed_until FROM market_watch_runs WHERE id = $1`, [r.runId]);
269
+ assert.deepEqual([runRow.status, runRow.reason, runRow.claimed_until], ['done', null, null]);
270
+ const { rows: [n] } = await q(
271
+ `SELECT (SELECT count(*)::int FROM market_watch_log_entries WHERE run_id = $1) AS entries,
272
+ (SELECT count(*)::int FROM market_watch_log_entries WHERE run_id = $1 AND source_url <> $2) AS unsourced,
273
+ (SELECT count(*)::int FROM market_watch_score_snapshots WHERE run_id = $1 AND reason = 'Moved closer.') AS scores,
274
+ (SELECT count(*)::int FROM market_watch_proposals WHERE run_id = $1 AND origin = 'run' AND kind = 'add_item') AS proposals,
275
+ (SELECT count(*)::int FROM market_watch_run_items WHERE run_id = $1 AND status = 'done' AND cost_usd = 0.075) AS items`,
276
+ [r.runId, NEWS]);
277
+ assert.equal(n.entries, r.done, 'one sourced entry per item; the invented link never landed');
278
+ assert.equal(n.unsourced, 0);
279
+ assert.equal(n.scores, r.done);
280
+ assert.equal(n.proposals, 1, 'one proposal per candidate per run, and no new item');
281
+ assert.equal(n.items, r.done);
282
+ assert.equal((await q(`SELECT count(*)::int AS n FROM market_watch_items WHERE project = $1 AND name = 'Newco Agents'`, [P])).rows[0].n, 0);
283
+ assert.equal(mirrored.length, r.done);
284
+ assert.ok(mirrored.every((m) => m.source === `market-watch:${r.runId}` && m.category === 'api' && m.amountUsd === 0.075));
285
+ // Recorded once: a second write of a finished item changes nothing.
286
+ const itemId = (await q(`SELECT item_id FROM market_watch_run_items WHERE run_id = $1 LIMIT 1`, [r.runId])).rows[0].item_id;
287
+ assert.equal((await store.recordItem(pool, P, r.runId, itemId, { ok: false, error: 'x', costUsd: 1 })).recorded, false);
288
+ // Never another project's: the same run and item under another project key writes nothing.
289
+ await assert.rejects(store.recordItem(pool, OTHER, r.runId, itemId, { ok: true, entries: [], scores: [], candidates: [], costUsd: 0 }), /cross_project/);
290
+ // The run left no open row behind for the next tick to resume.
291
+ assert.equal(await store.openRun(pool, P), null);
292
+ });
293
+
294
+ await check('one open run per project, and the key must be a secret-box blob', async () => {
295
+ await q(`INSERT INTO market_watch_runs (project, status, trigger) VALUES ($1, 'queued', 'manual')`, [OTHER]);
296
+ await refused(`INSERT INTO market_watch_runs (project, status, trigger) VALUES ($1, 'running', 'poller')`, [OTHER], 'a second open run');
297
+ await refused(`INSERT INTO market_watch_settings (project, anthropic_key_enc, anthropic_key_last4) VALUES ($1, 'sk-ant-plaintext', 'text')`, [OTHER], 'a plaintext key');
298
+ });
299
+ } finally {
300
+ await cleanup();
301
+ await pool.end();
302
+ }
303
+
304
+ if (failed) {
305
+ console.log(`market_watch_data_db.mjs: ${failed} failed`);
306
+ process.exit(1);
307
+ }
308
+ console.log('market_watch_data_db.mjs: all passed');
@@ -0,0 +1,114 @@
1
+ // tests/market_watch_module.mjs — the market-watch module is registered, accepted by
2
+ // the loader, and an instance boots with it present and DISABLED (task 1004935, goal
3
+ // 1000137). DB-free, so it runs in the unit lane.
4
+ //
5
+ // The module mounts no routes yet; task 1004936 gave it its tables and a one-shot hub seed
6
+ // poller (tests/market_watch_data.mjs covers those). What this file
7
+ // pins is everything the later Market Watch tasks will stand on:
8
+ // 1. the manifest validates, ships default:false, and declares spend.requiresPayer;
9
+ // 2. the loader discovers it from the live tree with no error and no skip;
10
+ // 3. the resolved module set has it OFF with no config, and the spend guard refuses
11
+ // to turn it on behind an open join door with no payer;
12
+ // 4. the real platform app boots and its hall reports market-watch present and false;
13
+ // 5. the creation picker does not offer it while it does nothing.
14
+ import assert from 'node:assert/strict';
15
+ import { test, before, after } from 'node:test';
16
+ import http from 'node:http';
17
+ import { readFileSync } from 'node:fs';
18
+ import { createRequire } from 'node:module';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const KEY = 'market-watch';
22
+ const manifest = JSON.parse(readFileSync(new URL('../modules/market-watch/module.json', import.meta.url), 'utf8'));
23
+ const { validateManifest } = require('../src/module-loader/manifest-schema.js');
24
+ const loader = require('../src/module-loader/loader.js');
25
+ const mods = require('../src/modules.js');
26
+ const bundles = require('../modules/provisioning/starter-bundles.js');
27
+ const scopeMap = require('../src/bongos/module-scope-map.js');
28
+
29
+ test('the manifest validates, ships disabled, declares a payer, and contributes only its data', () => {
30
+ const v = validateManifest(manifest);
31
+ assert.equal(v.valid, true, `manifest errors: ${v.errors.join('; ')}`);
32
+ assert.equal(manifest.key, KEY);
33
+ assert.equal(manifest.default, false, 'shipped is not enabled (ADR 0083)');
34
+ assert.equal(manifest.spend && manifest.spend.requiresPayer, true,
35
+ 'every run spends the project AI budget, so the payer guard is declared before the first route');
36
+ // Task 1004936 added its tables and the one-shot hub seed; task 1004937 the weekly-run
37
+ // poller. There are still no routes.
38
+ assert.deepEqual(manifest.contributes, { migrations: true, pollers: ['hub-seed', 'weekly-run'] },
39
+ 'its tables, the hub seed and the weekly run, nothing to mount');
40
+ });
41
+
42
+ test('the loader discovers it from the live tree — loaded, not an error, not skipped', () => {
43
+ loader._reset();
44
+ const snap = loader.loaderSnapshot();
45
+ assert.ok(snap.loaded.includes(KEY), `loaded: ${snap.loaded.join(', ')}`);
46
+ assert.deepEqual(snap.errors.filter((e) => JSON.stringify(e).includes(KEY)), []);
47
+ assert.deepEqual(snap.skipped.filter((s) => JSON.stringify(s).includes(KEY)), []);
48
+ const reg = loader.discoveredRegistry();
49
+ assert.equal(reg[KEY].default, false);
50
+ assert.equal(mods.metersSpend(reg[KEY]), true, 'the declaration rides through the loader to the spend guard');
51
+ loader._reset();
52
+ });
53
+
54
+ test('with no config it resolves OFF, and isModuleEnabled answers rather than throwing "unknown module"', () => {
55
+ mods._resetForTests && mods._resetForTests();
56
+ assert.ok(mods.knownModules().includes(KEY));
57
+ const set = mods.resolveModules({ registry: mods.effectiveRegistry() });
58
+ assert.equal(set[KEY], false);
59
+ assert.equal(mods.isModuleEnabled(KEY, set), false);
60
+ });
61
+
62
+ test('the spend guard bites it: an open door with no payer cannot enable it; a closed door can', () => {
63
+ const registry = mods.effectiveRegistry();
64
+ const on = { modules: { [KEY]: true } };
65
+ const silent = () => {};
66
+ assert.equal(mods.resolveModules({ instance: on, registry, spendGuard: { joinDoorOpen: true, payer: null }, warn: silent })[KEY], false);
67
+ assert.equal(mods.resolveModules({ instance: on, registry, spendGuard: { joinDoorOpen: false, payer: null }, warn: silent })[KEY], true);
68
+ });
69
+
70
+ test('it has a scope-roster key, and the creation picker does not offer it while it is empty', () => {
71
+ assert.ok(Array.isArray(scopeMap.MODULE_GLOBS[KEY]) && scopeMap.MODULE_GLOBS[KEY].includes('modules/market-watch/'));
72
+ // Every entry is an exact path or a dir prefix (the touches[] convention has no
73
+ // wildcards), and this test's own path resolves to the module's scope.
74
+ for (const g of scopeMap.MODULE_GLOBS[KEY]) assert.ok(!g.includes('*'), `wildcard glob never matches: ${g}`);
75
+ assert.ok(scopeMap.MODULE_GLOBS[KEY].includes('tests/market_watch_module.mjs'));
76
+ assert.ok(bundles.HUB_ONLY_MODULES.includes(KEY));
77
+ assert.ok(!bundles.OPTIONAL_MODULES.some((m) => m.key === KEY));
78
+ assert.ok(!bundles.ALWAYS_ON_CORE.includes(KEY));
79
+ });
80
+
81
+ // --- an instance boots with it present and disabled -------------------------
82
+ let server;
83
+ let port;
84
+ before(async () => {
85
+ loader._reset();
86
+ mods._resetForTests && mods._resetForTests();
87
+ const { buildPlatformApp } = require('../src/platform-server.js');
88
+ server = http.createServer(buildPlatformApp());
89
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
90
+ port = server.address().port;
91
+ });
92
+ after(async () => { if (server) await new Promise((resolve) => server.close(resolve)); });
93
+
94
+ function get(pathname, host) {
95
+ return new Promise((resolve, reject) => {
96
+ const req = http.request({ host: '127.0.0.1', port, path: pathname, method: 'GET', headers: { Host: host } }, (res) => {
97
+ let body = '';
98
+ res.on('data', (c) => (body += c));
99
+ res.on('end', () => resolve({ status: res.statusCode, body }));
100
+ });
101
+ req.on('error', reject);
102
+ req.end();
103
+ });
104
+ }
105
+
106
+ test('the platform boots and its hall reports market-watch present and OFF', async () => {
107
+ const res = await get('/', 'builders.test.local');
108
+ assert.equal(res.status, 200);
109
+ const m = res.body.match(/window\.__MODULES__=(\{.*?\});/);
110
+ assert.ok(m, 'a __MODULES__ assignment is present');
111
+ const enabled = JSON.parse(m[1]).enabled;
112
+ assert.ok(Object.prototype.hasOwnProperty.call(enabled, KEY), 'present: the booted instance knows the key');
113
+ assert.equal(enabled[KEY], false, 'disabled');
114
+ });