@bongos/core 1.23.22 → 1.24.2

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 (435) hide show
  1. package/.bongos-core.json +933 -413
  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 +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -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/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,235 @@
1
+ // tests/project_settings_spend_cap_route.mjs — PATCH /project-settings/spend-cap,
2
+ // executed for real (task 1004985, goal 1000094).
3
+ //
4
+ // The spend cap is the project's only monthly ceiling (task 1004712, ADR 0379), and
5
+ // until this file no test performed a write through its route: the neighbours pin
6
+ // refusal sentences and the act-alone inventory, never what the handler WRITES. So
7
+ // every case here asserts two things — the answer the caller got, and exactly what
8
+ // reached setSpendCap (nothing, on every refusal).
9
+ //
10
+ // What is real: the router (modules/lifecycle/routes/project-settings.js), the
11
+ // kernel's requirePermission (auth.js) and requireActAlone (act-alone.js), the
12
+ // body validator, the error envelope, and the pure parser + describer from
13
+ // src/bongos/project-settings.js. What is faked: who the caller is (requireBuilder),
14
+ // the two calls that would reach Postgres (setSpendCap records, resolveSpendCap
15
+ // reads back what was recorded), and the government port — the one place both
16
+ // gates ask "may this builder do this" — so a case can grant or withhold the atom
17
+ // and let the Treasury act alone or send it to a sitting.
18
+ //
19
+ // The doorway is stubbed in require.cache BEFORE the route loads (the
20
+ // tests/access_requests_invite.mjs pattern).
21
+ //
22
+ // Run: node --test tests/project_settings_spend_cap_route.mjs
23
+
24
+ import assert from 'node:assert/strict';
25
+ import { test, before, after, beforeEach } from 'node:test';
26
+ import http from 'node:http';
27
+ import { createRequire } from 'node:module';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const express = require('express');
31
+
32
+ const apiPath = require.resolve('../src/module-api.js');
33
+ const realApi = require(apiPath);
34
+ const realSettings = require('../src/bongos/project-settings.js');
35
+ const seams = require('../src/module-seams.js');
36
+
37
+ const ATOM = 'project.settings.manage';
38
+ const CALLER = 42;
39
+
40
+ // ── the world each case arranges ─────────────────────────────────────────────
41
+ let held = [ATOM]; // what the government port says the caller holds
42
+ let actAlone = { ok: true }; // what checkActAlone answers
43
+ let setThrows = null; // an Error setSpendCap raises, or null
44
+ let writes = []; // every setSpendCap(usd, builderId) call
45
+ let parses = []; // every parseSpendCap(raw) call
46
+ let actAloneAsked = []; // every checkActAlone(area, builderId) call
47
+ let stored = null; // what resolveSpendCap reads back
48
+
49
+ beforeEach(() => {
50
+ held = [ATOM];
51
+ actAlone = { ok: true };
52
+ setThrows = null;
53
+ writes = [];
54
+ parses = [];
55
+ actAloneAsked = [];
56
+ stored = null;
57
+ });
58
+
59
+ // The government port, as both gates reach it (seams.resolveOptional('government')).
60
+ // It is registered before any request and removed afterwards, so it cannot leak
61
+ // into another file's process — each test file runs in its own node.
62
+ const government = {
63
+ async resolveBuilderPermissions(builderId) { return builderId === CALLER ? held : []; },
64
+ async checkActAlone(area, builderId) {
65
+ actAloneAsked.push({ area, builderId });
66
+ if (actAlone instanceof Error) throw actAlone;
67
+ return actAlone;
68
+ },
69
+ };
70
+
71
+ require.cache[apiPath].exports = {
72
+ ...realApi,
73
+ requireBuilder: (req, _res, next) => { req.builder = { id: CALLER, github_login: 'treasurer' }; next(); },
74
+ projectSettings: {
75
+ ...realApi.projectSettings,
76
+ // The REAL parser, recorded, so "the parser's own reason" is the parser's own words.
77
+ parseSpendCap: (raw) => { parses.push(raw); return realSettings.parseSpendCap(raw); },
78
+ setSpendCap: async (usd, builderId) => {
79
+ writes.push({ usd, builderId });
80
+ if (setThrows) throw setThrows;
81
+ stored = usd;
82
+ return usd;
83
+ },
84
+ resolveSpendCap: async () => ({ on: stored != null, usd: stored, source: stored == null ? 'default' : 'project_settings', invalid: null }),
85
+ describeSpendCap: realSettings.describeSpendCap,
86
+ },
87
+ };
88
+
89
+ const buildRouter = require('../modules/lifecycle/routes/project-settings.js');
90
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
91
+
92
+ const app = express();
93
+ app.use(express.json());
94
+ app.use(attachFail);
95
+ app.use('/api/gds', buildRouter());
96
+
97
+ let server, port;
98
+ before(async () => {
99
+ seams.registerProvider('government', government);
100
+ server = http.createServer(app);
101
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
102
+ port = server.address().port;
103
+ });
104
+ after(async () => {
105
+ await new Promise((r) => server.close(r));
106
+ });
107
+
108
+ function patch(body) {
109
+ return new Promise((resolve, reject) => {
110
+ const payload = JSON.stringify(body);
111
+ const req = http.request({
112
+ host: '127.0.0.1', port, path: '/api/gds/project-settings/spend-cap', method: 'PATCH',
113
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) },
114
+ }, (res) => {
115
+ let data = '';
116
+ res.on('data', (c) => { data += c; });
117
+ res.on('end', () => resolve({ status: res.statusCode, json: data ? JSON.parse(data) : null }));
118
+ });
119
+ req.on('error', reject);
120
+ req.write(payload);
121
+ req.end();
122
+ });
123
+ }
124
+
125
+ // ── setting and clearing ─────────────────────────────────────────────────────
126
+
127
+ test('{usd: 50} sets the cap for the caller and answers it back as on', async () => {
128
+ const r = await patch({ usd: 50 });
129
+ assert.equal(r.status, 200);
130
+ assert.deepEqual(writes, [{ usd: 50, builderId: CALLER }], 'one write, the parsed dollars, by the caller');
131
+ assert.equal(r.json.ok, true);
132
+ assert.equal(r.json.on, true);
133
+ assert.equal(r.json.usd, 50);
134
+ assert.equal(r.json.sentence, realSettings.describeSpendCap({ on: true, usd: 50 }), 'the describer’s own sentence, not a re-typed one');
135
+ assert.deepEqual(parses, [50], 'a number goes through the shared parser');
136
+ assert.deepEqual(actAloneAsked, [{ area: 'treasury', builderId: CALLER }], 'the Treasury’s row decides, asked about this caller');
137
+ });
138
+
139
+ test('{usd: null} turns the cap off — setSpendCap(null), and the answer says off', async () => {
140
+ stored = 50;
141
+ const r = await patch({ usd: null });
142
+ assert.equal(r.status, 200);
143
+ assert.deepEqual(writes, [{ usd: null, builderId: CALLER }]);
144
+ assert.equal(r.json.on, false);
145
+ assert.equal(r.json.usd, null);
146
+ assert.equal(r.json.sentence, realSettings.describeSpendCap({ on: false, usd: null }));
147
+ assert.deepEqual(parses, [], 'null is the off switch, never handed to the parser');
148
+ });
149
+
150
+ // ── bad bodies: refused, nothing written ─────────────────────────────────────
151
+
152
+ test('{} → 400 bad_spend_cap and no write', async () => {
153
+ const r = await patch({});
154
+ assert.equal(r.status, 400);
155
+ assert.equal(r.json.error.code, 'bad_spend_cap');
156
+ // The route's own sentence, not the parser's: a missing usd is refused before any
157
+ // parse, so an absent field can never be read as "turn the cap off".
158
+ assert.equal(r.json.error.message, 'Send a number of dollars a month, or nothing to turn the cap off.');
159
+ assert.deepEqual(parses, [], 'refused before the parser is consulted');
160
+ assert.deepEqual(writes, []);
161
+ });
162
+
163
+ test("{usd: 'lots'} → 400 carrying the parser's own reason, and no write", async () => {
164
+ const r = await patch({ usd: 'lots' });
165
+ assert.equal(r.status, 400);
166
+ assert.equal(r.json.error.code, 'bad_spend_cap');
167
+ assert.deepEqual(parses, ['lots']);
168
+ const own = realSettings.parseSpendCap('lots').reason;
169
+ assert.ok(own, 'the parser refuses it');
170
+ assert.equal(r.json.error.message, own);
171
+ assert.deepEqual(writes, []);
172
+ });
173
+
174
+ // ── who may do it ────────────────────────────────────────────────────────────
175
+
176
+ test('a caller without project.settings.manage → 403 and no write, set or clear', async () => {
177
+ held = ['some.other.atom'];
178
+ for (const body of [{ usd: 50 }, { usd: null }]) {
179
+ const r = await patch(body);
180
+ assert.equal(r.status, 403, JSON.stringify(body));
181
+ assert.equal(r.json.error.code, 'permission_forbidden');
182
+ assert.match(r.json.error.message, /project\.settings\.manage/);
183
+ }
184
+ assert.deepEqual(writes, []);
185
+ assert.deepEqual(actAloneAsked, [], 'the atom gate refuses before the Treasury is even asked');
186
+ });
187
+
188
+ test('authority that cannot be established (the resolver throws) → 403 and no write', async () => {
189
+ held = null;
190
+ government.resolveBuilderPermissions = async () => { throw new Error('resolver down'); };
191
+ try {
192
+ const r = await patch({ usd: 50 });
193
+ assert.equal(r.status, 403);
194
+ assert.equal(r.json.error.code, 'permission_forbidden');
195
+ assert.deepEqual(writes, []);
196
+ } finally {
197
+ government.resolveBuilderPermissions = async (builderId) => (builderId === CALLER ? held : []);
198
+ }
199
+ });
200
+
201
+ test("requireActAlone('treasury') sending it to a sitting → its 409 refusal and no write", async () => {
202
+ actAlone = {
203
+ ok: false, reason: 'decided_by_sitting', area: 'treasury', rule: 'sitting',
204
+ who: 'the Treasury’s sitting', what_this_means: 'put it to a vote', open_with: '/government/treasury',
205
+ };
206
+ for (const body of [{ usd: 50 }, { usd: null }]) {
207
+ const r = await patch(body);
208
+ assert.equal(r.status, 409, JSON.stringify(body));
209
+ assert.equal(r.json.error.code, 'decided_by_sitting');
210
+ assert.equal(r.json.error.details.area, 'treasury');
211
+ assert.equal(r.json.error.details.open_with, '/government/treasury', 'the gate’s own refusal, passed through');
212
+ }
213
+ assert.deepEqual(writes, []);
214
+ });
215
+
216
+ test('a Treasury rule that cannot be read → 503 and no write (fail closed)', async () => {
217
+ actAlone = new Error('rule table unreadable');
218
+ const r = await patch({ usd: 50 });
219
+ assert.equal(r.status, 503);
220
+ assert.equal(r.json.error.code, 'decision_rule_unreadable');
221
+ assert.deepEqual(writes, []);
222
+ });
223
+
224
+ // ── the write failing ────────────────────────────────────────────────────────
225
+
226
+ test('setSpendCap throwing → 500 project_settings_write_failed, the error’s text kept off the wire', async () => {
227
+ setThrows = new Error('relation project_settings: connection reset SECRET-ISH-DETAIL');
228
+ const r = await patch({ usd: 50 });
229
+ assert.equal(r.status, 500);
230
+ assert.equal(r.json.error.code, 'project_settings_write_failed');
231
+ assert.equal(r.json.error.message, 'internal error');
232
+ assert.doesNotMatch(JSON.stringify(r.json), /SECRET-ISH-DETAIL|connection reset/, 'the database error never reaches the caller');
233
+ assert.equal(r.json.ok, undefined, 'no success shape rides on a failure');
234
+ assert.deepEqual(writes, [{ usd: 50, builderId: CALLER }], 'it was attempted once, not retried');
235
+ });
@@ -240,7 +240,35 @@ await test('a throwing pool is reported, not propagated (a sweep can never crash
240
240
  await test('the sweep runs on the tick, fire-and-forget like its siblings', async () => {
241
241
  const src = readFileSync(new URL('../modules/lifecycle/publish-reconciler.js', import.meta.url), 'utf8')
242
242
  .replace(/\r\n/g, '\n');
243
- assert.match(src, /runPromotionSweep\(deps\)\.catch\(/, 'wired into tick() the way runCriterionSweep is');
243
+ // task 1004924: the tick runs a guarded list (buildTick skips a sweep still in
244
+ // flight), so the wiring is an entry in SWEEPS, beside runCriterionSweep.
245
+ // task 1004965: the promotion sweep runs HOURLY; the criterion sweep keeps the tick.
246
+ assert.match(src, /\['promotion sweep', runPromotionSweep, HOURLY_MS\]/, 'wired into the tick, hourly');
247
+ assert.match(src, /\['criterion sweep', runCriterionSweep\]/);
248
+ });
249
+
250
+ await test('cadence per sweep: publish, cost and criterion every tick; promotion and craft backfill hourly (task 1004965)', async () => {
251
+ const byName = new Map(reconciler.SWEEPS.map(([name, , everyMs]) => [name, everyMs]));
252
+ assert.equal(reconciler.HOURLY_MS, 60 * 60_000);
253
+ for (const name of ['sweep', 'cost sweep', 'criterion sweep']) assert.equal(byName.get(name), undefined, `${name} keeps the tick`);
254
+ for (const name of ['promotion sweep', 'craft achievement backfill']) assert.equal(byName.get(name), reconciler.HOURLY_MS, `${name} is hourly`);
255
+
256
+ // Drive buildTick through 2 hours of 5-minute ticks on a fake clock.
257
+ const runs = { fast: 0, hourlyAt: [] };
258
+ let t = 0;
259
+ let i = 0;
260
+ const sweeps = [
261
+ ['fast', async () => { runs.fast += 1; }],
262
+ ['hourly', async () => { runs.hourlyAt.push(i); }, reconciler.HOURLY_MS],
263
+ ];
264
+ const tick = reconciler.buildTick({ logFn: () => {}, errFn: () => {} }, sweeps, { now: () => t, tickMs: 5 * 60_000 });
265
+ for (i = 0; i < 24; i++) {
266
+ tick();
267
+ await new Promise((r) => setImmediate(r)); // let the fire-and-forget runs settle
268
+ t += 5 * 60_000 - (i % 2 ? 3 : 0); // a few ms early on some ticks: timer jitter
269
+ }
270
+ assert.equal(runs.fast, 24, 'every tick');
271
+ assert.deepEqual(runs.hourlyAt, [0, 12], 'on the first tick and exactly 12 ticks (an hour) later — jitter never costs it a tick');
244
272
  });
245
273
 
246
274
  // ---------- Summary ----------
@@ -1628,7 +1628,8 @@ t('webEnvBody writes the BARE publish-credential vars, pem flattened to one env
1628
1628
  });
1629
1629
  // BARE names (github-push.js reads process.env.GITHUB_APP_ID, no instance prefix)
1630
1630
  assert.match(body, /^GITHUB_APP_ID=42$/m);
1631
- assert.match(body, /^GITHUB_APP_PRIVATE_KEY=-----BEGIN KEY-----\\nAAA\\nBBB\\n-----END KEY-----\\n$/m);
1631
+ // double-quoted so systemd keeps the \n (task 1005009; tests/github_one_trip.mjs parses it as systemd does)
1632
+ assert.match(body, /^GITHUB_APP_PRIVATE_KEY="-----BEGIN KEY-----\\nAAA\\nBBB\\n-----END KEY-----\\n"$/m);
1632
1633
  assert.doesNotMatch(body, /GITHUB_APP_INSTALLATION_ID/, 'installation id is discovered at runtime, never placed');
1633
1634
  // pem missing (older Apps) → no half-written credential
1634
1635
  const noPem = P.webEnvBody(inst, { clientId: 'Iv1.abc123def456ghi7', clientSecret: 's'.repeat(40), appId: 42, appPem: null });
@@ -1683,7 +1684,7 @@ await ta('placeManifestCreds (--apply): converts, rewrites web.env with the cred
1683
1684
  assert.match(env.body, /_GITHUB_CLIENT_SECRET=z{40}/);
1684
1685
  // …and the publish credential (task 1003049): App id + one-line pem, bare names
1685
1686
  assert.match(env.body, /^GITHUB_APP_ID=9$/m, 'the App id is placed as the publish credential');
1686
- assert.match(env.body, /^GITHUB_APP_PRIVATE_KEY=-----BEGIN KEY-----\\nAAA\\n-----END KEY-----\\n$/m, 'the pem is placed env-flattened');
1687
+ assert.match(env.body, /^GITHUB_APP_PRIVATE_KEY="-----BEGIN KEY-----\\nAAA\\n-----END KEY-----\\n"$/m, 'the pem is placed env-flattened, double-quoted (task 1005009)');
1687
1688
  // the unit was restarted, and /healthz was probed
1688
1689
  assert.ok(cmds.some((c) => /systemctl restart mercury/.test(c)), 'restarts the unit');
1689
1690
  assert.ok(cmds.some((c) => /\/healthz/.test(c)), 'probes healthz');
@@ -219,6 +219,24 @@ await ta('the knocks run a few at a time, never all at once and never one by one
219
219
  assert.deepEqual(out, { checked: 12, live: 12 });
220
220
  assert.equal(peak, 5);
221
221
  });
222
+ await ta('cadence: every 5 minutes, not every minute (task 1004960)', () => {
223
+ assert.equal(liveness.INTERVAL_MS, 5 * 60_000);
224
+ });
225
+ await ta('a run that starts while the previous sweep is in flight is skipped (task 1004960)', async () => {
226
+ let reads = 0;
227
+ let release;
228
+ const gate = new Promise((r) => { release = r; });
229
+ const db = { query: async () => { reads++; await gate; return { rows: [] }; } };
230
+ const first = liveness.tick({ pool: db, log: () => {} });
231
+ const secondRun = liveness.tick({ pool: db, log: () => {} }); // fires while the first is still reading
232
+ release();
233
+ const [firstOut, second] = await Promise.all([first, secondRun]);
234
+ assert.deepEqual(second, { skipped: true });
235
+ assert.deepEqual(firstOut, { checked: 0, live: 0 });
236
+ assert.equal(reads, 1, 'the overlapping run never touched the database');
237
+ assert.deepEqual(await liveness.tick({ pool: { query: async () => ({ rows: [] }) }, log: () => {} }), { checked: 0, live: 0 },
238
+ 'once the first finished, the next run goes ahead');
239
+ });
222
240
  await ta('the module starts the poller', () => {
223
241
  const manifest = JSON.parse(readFileSync(new URL('../modules/provisioning/module.json', import.meta.url), 'utf8'));
224
242
  assert.ok(manifest.contributes.pollers.includes('app-liveness'));
@@ -130,6 +130,59 @@ await ta('GitHub unreachable → ADMITTED, matching the runner: an outage must n
130
130
  assert.equal(await adoptCredentialFault(adopt(), deps({ throws: 'github_error' })), null);
131
131
  });
132
132
 
133
+ // ── pre-launch (stealth) keeps project code private (task 1004998) ──────────────
134
+
135
+ await ta('pre-launch ON: a PUBLIC repo is refused 409 public_repo_prelaunch, saying how to proceed', async () => {
136
+ const d = { ...deps({ repo: { private: false, admin: true } }), prelaunchActive: () => true };
137
+ const fault = await adoptCredentialFault(adopt(), d);
138
+ assert.equal(fault && fault.code, 'public_repo_prelaunch');
139
+ assert.equal(fault.status, 409);
140
+ assert.match(fault.message, /octo-owner\/demo is public/);
141
+ assert.match(fault.message, /private on GitHub first/, 'names the way forward');
142
+ });
143
+
144
+ await ta('pre-launch ON: a PRIVATE repo with admin still adopts, exactly as today', async () => {
145
+ const d = { ...deps({ repo: { private: true, admin: true } }), prelaunchActive: () => true };
146
+ assert.equal(await adoptCredentialFault(adopt(), d), null);
147
+ });
148
+
149
+ await ta('pre-launch ON: the admin refusal stops offering "make it public" as the way out', async () => {
150
+ const d = { ...deps({ repo: { private: true, admin: false } }), prelaunchActive: () => true };
151
+ const fault = await adoptCredentialFault(adopt(), d);
152
+ assert.equal(fault && fault.code, 'repo_admin_required');
153
+ assert.doesNotMatch(fault.message, /make the repository public/);
154
+ });
155
+
156
+ await ta('pre-launch ON: a GitHub hiccup still ADMITS — no positive read that the repo is public', async () => {
157
+ const d = { ...deps({ throws: 'github_error' }), prelaunchActive: () => true };
158
+ assert.equal(await adoptCredentialFault(adopt(), d), null);
159
+ });
160
+
161
+ await ta('pre-launch OFF (or not wired): a public repo adopts as before', async () => {
162
+ assert.equal(await adoptCredentialFault(adopt(), { ...deps(), prelaunchActive: () => false }), null);
163
+ assert.equal(await adoptCredentialFault(adopt(), deps()), null);
164
+ });
165
+
166
+ await ta('the doorway hands the module the REAL gate switch, read from the env each call', async () => {
167
+ const api = require('../src/module-api.js');
168
+ const saved = [process.env.PRELAUNCH_PASSWORD, process.env.STEALTH_PASSWORD];
169
+ try {
170
+ delete process.env.PRELAUNCH_PASSWORD; delete process.env.STEALTH_PASSWORD;
171
+ assert.equal(api.prelaunchActive(), false);
172
+ process.env.STEALTH_PASSWORD = 'x'; // the deprecated alias live still sets
173
+ assert.equal(api.prelaunchActive(), true);
174
+ } finally {
175
+ if (saved[0] === undefined) delete process.env.PRELAUNCH_PASSWORD; else process.env.PRELAUNCH_PASSWORD = saved[0];
176
+ if (saved[1] === undefined) delete process.env.STEALTH_PASSWORD; else process.env.STEALTH_PASSWORD = saved[1];
177
+ }
178
+ });
179
+
180
+ await ta('the create route WIRES the switch in — without it the check above is never asked', async () => {
181
+ const src = require('node:fs').readFileSync(require.resolve('../modules/provisioning/routes/provisioning.js'), 'utf8');
182
+ const call = src.slice(src.indexOf('await adoptCredentialFault('), src.indexOf('if (credFault)'));
183
+ assert.match(call, /prelaunchActive:\s*api\.prelaunchActive/);
184
+ });
185
+
133
186
  // ── the parse must not drift from the runner's ──────────────────────────────────
134
187
 
135
188
  await ta('parseTargetRef accepts every form the runner accepts, and rejects what it rejects', async () => {
@@ -0,0 +1,223 @@
1
+ // tests/provisioning_env_manifest_edit_delete.mjs — PATCH and DELETE
2
+ // /provisioning/instances/:id/env-manifest/:varId, executed for real (task 1004986,
3
+ // goal 1000094). The POST path is tests/provisioning_env_manifest.mjs's.
4
+ //
5
+ // The two write routes after a declaration exists had no test driving them, and each
6
+ // carries a promise a regression would break quietly:
7
+ // - a VALUE is refused out loud, never stored (the manifest holds names only);
8
+ // - a variable id that belongs to ANOTHER project answers exactly like an id that
9
+ // does not exist, so a guessed id never confirms what someone else declared;
10
+ // - a builder who neither owns the project nor is an Archon is turned away before
11
+ // any data call, with the same 404 a missing project gets;
12
+ // - the project the data call is scoped to comes from the owner gate's loaded row,
13
+ // never from anything the caller put in the body.
14
+ // Every refusal below asserts that NO data call was made.
15
+ //
16
+ // What is real: the router (modules/provisioning/routes/env-manifest.js), the owner
17
+ // gate (route-helpers.js requireProjectOwner), parseVarId, the value refusal, the
18
+ // strict body validator and the kernel error envelope. What is faked: who the caller
19
+ // is (requireBuilder), the project row (getInstanceById: project 1, owned by builder
20
+ // 7), and the two data calls, which RECORD their arguments and answer the way the SQL
21
+ // does — a row only when the declaration's instance_id matches the scoped project.
22
+ //
23
+ // Run: node --test tests/provisioning_env_manifest_edit_delete.mjs
24
+
25
+ import assert from 'node:assert/strict';
26
+ import { test, before, after, beforeEach } from 'node:test';
27
+ import http from 'node:http';
28
+ import { createRequire } from 'node:module';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const express = require('express');
32
+
33
+ const api = require('../src/module-api.js');
34
+ const provisioning = require('../modules/provisioning/provisioning.js');
35
+ const envManifest = require('../modules/provisioning/env-manifest.js');
36
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
37
+
38
+ // ── the cast ─────────────────────────────────────────────────────────────────
39
+ const OWNER = { id: 7, rank: 'xenos', github_login: 'owner' };
40
+ const STRANGER = { id: 8, rank: 'metic', github_login: 'stranger' };
41
+ const ARCHON = { id: 9, rank: 'archon', github_login: 'archon' };
42
+ const PROJECT_1 = Object.freeze({ id: 1, owner_builder_id: 7, slug: 'lantern', status: 'active', hosting_shape: 'byo-host' });
43
+
44
+ // Declarations as the table holds them: 5 is project 1's, 6 is project 2's.
45
+ const DECLARED = new Map([
46
+ ['5', { id: 5, instance_id: 1, name: 'DATABASE_URL', description: 'old text', environments: ['production'], required: true, created_at: 'c', updated_at: 'u' }],
47
+ ['6', { id: 6, instance_id: 2, name: 'OTHER_PROJECTS_VAR', description: 'theirs', environments: ['production'], required: true, created_at: 'c', updated_at: 'u' }],
48
+ ]);
49
+
50
+ let caller = OWNER;
51
+ let loads = [];
52
+ let updates = [];
53
+ let removes = [];
54
+
55
+ beforeEach(() => { caller = OWNER; loads = []; updates = []; removes = []; });
56
+
57
+ api.requireBuilder = (req, _res, next) => { req.builder = caller; next(); };
58
+ provisioning.getInstanceById = async (_db, id) => {
59
+ loads.push(String(id));
60
+ return String(id) === '1' ? { ...PROJECT_1 } : null;
61
+ };
62
+ // The WHERE instance_id = $1 AND id = $2 the real SQL applies, applied here.
63
+ const scoped = (instanceId, varId) => {
64
+ const row = DECLARED.get(String(varId));
65
+ return row && String(row.instance_id) === String(instanceId) ? row : null;
66
+ };
67
+ envManifest.updateEnvVar = async (db, instanceId, varId, patch) => {
68
+ updates.push({ db, instanceId, varId, patch: { ...patch } });
69
+ const row = scoped(instanceId, varId);
70
+ return row ? envManifest.envVarRow({ ...row, ...patch }) : null;
71
+ };
72
+ envManifest.removeEnvVar = async (db, instanceId, varId) => {
73
+ removes.push({ db, instanceId, varId });
74
+ return !!scoped(instanceId, varId);
75
+ };
76
+
77
+ const buildRouter = require('../modules/provisioning/routes/env-manifest.js');
78
+ const app = express();
79
+ app.use(express.json());
80
+ app.use(attachFail);
81
+ app.use('/api/bongos', buildRouter());
82
+
83
+ let server, base;
84
+ before(async () => {
85
+ server = http.createServer(app);
86
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
87
+ base = `http://127.0.0.1:${server.address().port}/api/bongos`;
88
+ });
89
+ after(async () => { await new Promise((r) => server.close(r)); });
90
+
91
+ function call(method, path, body) {
92
+ return new Promise((resolve, reject) => {
93
+ const payload = body === undefined ? null : JSON.stringify(body);
94
+ const headers = payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } : {};
95
+ const req = http.request(`${base}${path}`, { method, headers }, (res) => {
96
+ let raw = '';
97
+ res.on('data', (c) => { raw += c; });
98
+ res.on('end', () => { let json = null; try { json = JSON.parse(raw); } catch { /* not JSON */ } resolve({ status: res.statusCode, json, raw }); });
99
+ });
100
+ req.on('error', reject);
101
+ if (payload) req.write(payload);
102
+ req.end();
103
+ });
104
+ }
105
+ const varUrl = (varId, project = 1) => `/provisioning/instances/${project}/env-manifest/${varId}`;
106
+
107
+ // Obviously fake: the shape of a provider key, none of its substance.
108
+ const FAKE_SECRET = 'sk-FAKE-not-a-real-key-000000';
109
+
110
+ // ── PATCH ────────────────────────────────────────────────────────────────────
111
+
112
+ test('PATCH {value} → 400 value_not_accepted, the value never echoed, no updateEnvVar call', async () => {
113
+ const r = await call('PATCH', varUrl(5), { value: FAKE_SECRET });
114
+ assert.equal(r.status, 400);
115
+ assert.equal(r.json.error.code, 'value_not_accepted', 'the value refusal speaks, not the generic validator');
116
+ assert.doesNotMatch(r.raw, /sk-FAKE/, 'the secret is not reflected back');
117
+ assert.deepEqual(updates, []);
118
+ });
119
+
120
+ test('PATCH carrying a value under every spelling the manifest knows, beside a legal edit → refused, no call', async () => {
121
+ for (const field of envManifest.VALUE_FIELDS) {
122
+ const r = await call('PATCH', varUrl(5), { description: 'fine', [field]: FAKE_SECRET });
123
+ assert.equal(r.status, 400, field);
124
+ assert.equal(r.json.error.code, 'value_not_accepted', field);
125
+ }
126
+ assert.deepEqual(updates, []);
127
+ });
128
+
129
+ test("PATCH another project's varId → 404 env_var_not_found, byte-identical to a varId that does not exist", async () => {
130
+ const foreign = await call('PATCH', varUrl(6), { description: 'mine now' });
131
+ const missing = await call('PATCH', varUrl(999), { description: 'mine now' });
132
+ assert.equal(foreign.status, 404);
133
+ assert.equal(foreign.json.error.code, 'env_var_not_found');
134
+ assert.equal(foreign.raw, missing.raw, 'a guessed id confirms nothing about another project');
135
+ assert.doesNotMatch(foreign.raw, /OTHER_PROJECTS_VAR|theirs/);
136
+ assert.deepEqual(updates.map((u) => u.instanceId), [1, 1], 'both scoped to the caller’s project, never to the id’s owner');
137
+ });
138
+
139
+ test('PATCH as builder 8 (not owner, not Archon) → 404 instance_not_found and no updateEnvVar call', async () => {
140
+ caller = STRANGER;
141
+ const r = await call('PATCH', varUrl(5), { description: 'x' });
142
+ assert.equal(r.status, 404);
143
+ assert.equal(r.json.error.code, 'instance_not_found');
144
+ assert.deepEqual(updates, []);
145
+ // …and byte-identical to a project that does not exist, so the id is not confirmed.
146
+ const none = await call('PATCH', varUrl(5, 77), { description: 'x' });
147
+ assert.equal(r.raw, none.raw);
148
+ });
149
+
150
+ test('PATCH as an Archon on project 1 → 200, scoped to project 1', async () => {
151
+ caller = ARCHON;
152
+ const r = await call('PATCH', varUrl(5), { description: 'archon fixed it' });
153
+ assert.equal(r.status, 200, r.raw);
154
+ assert.equal(r.json.env_var.description, 'archon fixed it');
155
+ assert.equal(r.json.values_held, false);
156
+ assert.equal(updates.length, 1);
157
+ assert.equal(updates[0].instanceId, 1);
158
+ });
159
+
160
+ test('PATCH with only description → environments (and the rest) absent from the update call', async () => {
161
+ const r = await call('PATCH', varUrl(5), { description: 'new text' });
162
+ assert.equal(r.status, 200, r.raw);
163
+ assert.equal(updates.length, 1);
164
+ const u = updates[0];
165
+ assert.equal(u.db, api.pool, 'the module pool, not something the request supplied');
166
+ assert.equal(u.instanceId, 1);
167
+ assert.equal(u.varId, '5');
168
+ assert.deepEqual(Object.keys(u.patch), ['description']);
169
+ assert.equal(Object.prototype.hasOwnProperty.call(u.patch, 'environments'), false, 'an omitted field is left alone (merge-patch)');
170
+ });
171
+
172
+ test('PATCH a malformed varId → 400 bad_id and no call', async () => {
173
+ const r = await call('PATCH', varUrl('5abc'), { description: 'x' });
174
+ assert.equal(r.status, 400);
175
+ assert.equal(r.json.error.code, 'bad_id');
176
+ assert.deepEqual(updates, []);
177
+ });
178
+
179
+ // ── DELETE ───────────────────────────────────────────────────────────────────
180
+
181
+ test('DELETE → removeEnvVar(pool, 1, varId): the project id from the gate, never the body', async () => {
182
+ const r = await call('DELETE', varUrl(5), { instance_id: 2, instanceId: 2, id: 2 });
183
+ assert.equal(r.status, 200, r.raw);
184
+ assert.equal(r.json.removed, true);
185
+ assert.equal(removes.length, 1);
186
+ assert.equal(removes[0].db, api.pool);
187
+ assert.equal(removes[0].instanceId, 1, 'the loaded row’s id, not the body’s 2');
188
+ assert.equal(removes[0].varId, '5');
189
+ });
190
+
191
+ test("DELETE another project's varId → 404 env_var_not_found, byte-identical to a missing one", async () => {
192
+ const foreign = await call('DELETE', varUrl(6));
193
+ const missing = await call('DELETE', varUrl(999));
194
+ assert.equal(foreign.status, 404);
195
+ assert.equal(foreign.json.error.code, 'env_var_not_found');
196
+ assert.equal(foreign.raw, missing.raw);
197
+ assert.deepEqual(removes.map((x) => x.instanceId), [1, 1]);
198
+ });
199
+
200
+ test('DELETE as a non-owner → 404 instance_not_found before any call', async () => {
201
+ caller = STRANGER;
202
+ const r = await call('DELETE', varUrl(5));
203
+ assert.equal(r.status, 404);
204
+ assert.equal(r.json.error.code, 'instance_not_found');
205
+ assert.deepEqual(removes, []);
206
+ });
207
+
208
+ test('DELETE as an Archon on project 1 → 200, scoped to project 1', async () => {
209
+ caller = ARCHON;
210
+ const r = await call('DELETE', varUrl(5));
211
+ assert.equal(r.status, 200, r.raw);
212
+ assert.deepEqual(removes.map((x) => x.instanceId), [1]);
213
+ });
214
+
215
+ test('DELETE a malformed varId → 400 bad_id from parseVarId and no call', async () => {
216
+ for (const bad of ['abc', '5;DROP', '-1']) {
217
+ const r = await call('DELETE', varUrl(encodeURIComponent(bad)));
218
+ assert.equal(r.status, 400, bad);
219
+ assert.equal(r.json.error.code, 'bad_id', bad);
220
+ assert.match(r.json.error.message, /varId/, 'parseVarId’s own sentence, naming the field');
221
+ }
222
+ assert.deepEqual(removes, []);
223
+ });
@@ -0,0 +1,60 @@
1
+ // tests/publish_reconciler_overlap.mjs
2
+ //
3
+ // task 1004924 — the reconciler's interval tick fired five sweeps every 5 minutes
4
+ // with no check that the previous round had finished, so a slow DB or GitHub call
5
+ // stacked rounds and held several pool connections at once. buildTick now keeps a
6
+ // per-sweep in-flight flag: a sweep still running from the last tick is skipped.
7
+ //
8
+ // Its own file on purpose: tests/publish_reconciler.mjs exits on a fixed 100 ms
9
+ // timer, so a case appended there that waits on a few event-loop turns is cut off
10
+ // before it reports — it neither passes nor fails, it just vanishes.
11
+ //
12
+ // Run: node tests/publish_reconciler_overlap.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import { createRequire } from 'node:module';
16
+ import { makeRunner } from './helpers.mjs';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const reconciler = require('../modules/lifecycle/publish-reconciler.js');
20
+ const { test, summary } = makeRunner();
21
+
22
+ const settle = () => new Promise((r) => setImmediate(r));
23
+
24
+ await test('buildTick skips a sweep still in flight, and runs it again once it settles', async () => {
25
+ const calls = { slow: 0, fast: 0, failing: 0 };
26
+ let releaseSlow;
27
+ const sweeps = [
28
+ ['slow', () => { calls.slow++; return new Promise((r) => { releaseSlow = r; }); }],
29
+ ['fast', async () => { calls.fast++; }],
30
+ ['failing', async () => { calls.failing++; throw new Error('boom'); }],
31
+ ];
32
+ const logs = [];
33
+ const errs = [];
34
+ const tick = reconciler.buildTick({ log: (m) => logs.push(m), error: (m) => errs.push(m) }, sweeps);
35
+
36
+ tick(); await settle();
37
+ tick(); await settle();
38
+ tick(); await settle();
39
+ assert.equal(calls.slow, 1, 'the slow sweep is never stacked while its first run is pending');
40
+ assert.equal(calls.fast, 3, 'a slow sweep never starves the others');
41
+ assert.equal(calls.failing, 3, 'a rejected run clears its flag too');
42
+ assert.equal(logs.filter((m) => /slow still running/.test(m)).length, 2, 'each skip is logged');
43
+
44
+ releaseSlow(); await settle();
45
+ tick(); await settle();
46
+ assert.equal(calls.slow, 2, 'once the run settles the next tick runs it again');
47
+ });
48
+
49
+ await test('the default tick covers all five sweeps', () => {
50
+ // A sweep left out of the guarded list would simply never run again.
51
+ const src = require('node:fs').readFileSync(require.resolve('../modules/lifecycle/publish-reconciler.js'), 'utf8');
52
+ // task 1004965: read the exported list, since an entry may now carry a cadence.
53
+ const fns = reconciler.SWEEPS.map(([, fn]) => fn);
54
+ for (const fn of ['runSweep', 'runCostSweep', 'runCriterionSweep', 'runPromotionSweep', 'runCraftAchievementBackfill']) {
55
+ assert.ok(fns.includes(reconciler[fn]), `${fn} is in the tick`);
56
+ }
57
+ assert.match(src, /const tick = buildTick\(deps\);/, 'start() uses the guarded tick');
58
+ });
59
+
60
+ summary();