@bongos/core 1.23.21 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/.bongos-core.json +983 -428
  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 +6 -3
  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 +94 -0
  27. package/clients/bongos-client/index.cjs +94 -0
  28. package/clients/bongos-client/index.d.ts +154 -4
  29. package/clients/bongos-client/index.mjs +94 -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 +87 -0
  33. package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
  34. package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
  35. package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
  36. package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
  37. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  38. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  39. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  40. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  41. package/docs/adr/README.md +10 -1
  42. package/docs/api/openapi.json +8619 -5885
  43. package/docs/api-reference.md +68 -9
  44. package/docs/architecture.md +37 -1
  45. package/docs/canonical-permissions.md +1 -1
  46. package/docs/copy-inventory.md +420 -337
  47. package/docs/copy-registry.json +1321 -500
  48. package/docs/file-map.md +4 -0
  49. package/docs/module-api-changelog.md +7 -0
  50. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  51. package/docs/onboarding/diagrams/README.md +1 -1
  52. package/docs/onboarding/drachmae-and-karma.md +15 -15
  53. package/docs/onboarding/primer.md +2 -2
  54. package/docs/page-inventory.json +55 -6
  55. package/docs/page-readings.json +1185 -1011
  56. package/docs/recipes/google-sign-in.md +60 -0
  57. package/docs/recipes/ops-gotchas.md +1 -1
  58. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  59. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  60. package/migrations/core_283_payout_approvals.sql +183 -0
  61. package/migrations/core_284_payout_gates.sql +179 -0
  62. package/migrations/core_285_task_needs.sql +74 -0
  63. package/modules/ai-studio/CLAUDE.md +15 -0
  64. package/modules/ai-studio/billing.js +67 -0
  65. package/modules/ai-studio/context.js +29 -0
  66. package/modules/ai-studio/db.js +172 -0
  67. package/modules/ai-studio/gemini.js +75 -0
  68. package/modules/ai-studio/generate.js +119 -0
  69. package/modules/ai-studio/keys.js +73 -0
  70. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  71. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  72. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  73. package/modules/ai-studio/module.json +26 -0
  74. package/modules/ai-studio/openrouter.js +90 -0
  75. package/modules/ai-studio/payments.js +105 -0
  76. package/modules/ai-studio/pricing.js +92 -0
  77. package/modules/ai-studio/routes/ai-studio.js +252 -0
  78. package/modules/ai-studio/studio.js +121 -0
  79. package/modules/autonomy/fence.js +64 -1
  80. package/modules/builder-settings/builder-needs.js +27 -1
  81. package/modules/copy-desk/routes/copy-desk.js +5 -0
  82. package/modules/economy/achievements.js +22 -10
  83. package/modules/economy/cost-classify.js +9 -0
  84. package/modules/economy/cost-write-guard.js +52 -0
  85. package/modules/economy/cost.js +2 -1
  86. package/modules/economy/credits.js +18 -15
  87. package/modules/economy/goal-reward.js +161 -0
  88. package/modules/economy/reward.js +4 -0
  89. package/modules/economy/routes/cost.js +33 -7
  90. package/modules/government/catalog.js +6 -0
  91. package/modules/government/db.js +108 -0
  92. package/modules/government/docket.js +36 -0
  93. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  94. package/modules/government/pollers/board-expiry.js +108 -20
  95. package/modules/government/protected-surfaces.json +1 -0
  96. package/modules/government/routes/government.js +50 -18
  97. package/modules/government/routing.js +39 -5
  98. package/modules/government/waiting.js +74 -0
  99. package/modules/grading/grader-subagent.js +30 -11
  100. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  101. package/modules/hall-ui/public/ai-studio.css +193 -0
  102. package/modules/hall-ui/public/ai-studio.html +98 -0
  103. package/modules/hall-ui/public/ai-studio.js +556 -0
  104. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  105. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  106. package/modules/hall-ui/public/blockers-lib.js +15 -1
  107. package/modules/hall-ui/public/blockers.html +8 -0
  108. package/modules/hall-ui/public/blockers.js +1 -1
  109. package/modules/hall-ui/public/city.html +15 -15
  110. package/modules/hall-ui/public/claim-action.js +3 -0
  111. package/modules/hall-ui/public/collab.html +12 -0
  112. package/modules/hall-ui/public/craft-mode.js +4 -1
  113. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  114. package/modules/hall-ui/public/drachmae.html +1 -1
  115. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  120. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  121. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  122. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  123. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  124. package/modules/hall-ui/public/goal-inbox.js +1 -1
  125. package/modules/hall-ui/public/goals-render.js +4 -0
  126. package/modules/hall-ui/public/goals.html +8 -0
  127. package/modules/hall-ui/public/gov-living.js +6 -2
  128. package/modules/hall-ui/public/hall-ask.css +5 -0
  129. package/modules/hall-ui/public/hall-ask.js +39 -3
  130. package/modules/hall-ui/public/notes-panel.css +20 -0
  131. package/modules/hall-ui/public/notes-panel.js +146 -0
  132. package/modules/hall-ui/public/project-settings.html +1 -1
  133. package/modules/hall-ui/public/project-settings.js +7 -5
  134. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  135. package/modules/hall-ui/public/settings.html +15 -0
  136. package/modules/hall-ui/public/street-deck.js +12 -3
  137. package/modules/hall-ui/public/studio.css +90 -8
  138. package/modules/hall-ui/public/studio.html +39 -10
  139. package/modules/hall-ui/public/studio.js +148 -18
  140. package/modules/hall-ui/public/task.html +2 -0
  141. package/modules/hall-ui/public/task.js +4 -0
  142. package/modules/hall-ui/public/tweak-editor.js +12 -3
  143. package/modules/hall-ui/public/welcome-boot.css +42 -6
  144. package/modules/hall-ui/public/welcome-boot.js +20 -6
  145. package/modules/hall-ui/public/work.js +20 -0
  146. package/modules/hall-ui/records/ai-studio.md +36 -0
  147. package/modules/hall-ui/records/blockers-people.md +24 -0
  148. package/modules/lifecycle/db-analytics.js +10 -3
  149. package/modules/lifecycle/db-claims.js +32 -4
  150. package/modules/lifecycle/db-goals.js +48 -18
  151. package/modules/lifecycle/db-shared.js +9 -0
  152. package/modules/lifecycle/db-tasks.js +15 -6
  153. package/modules/lifecycle/db-versions.js +21 -8
  154. package/modules/lifecycle/done-when.js +23 -1
  155. package/modules/lifecycle/lifecycle.js +5 -0
  156. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  157. package/modules/lifecycle/module.json +2 -1
  158. package/modules/lifecycle/notes.js +219 -0
  159. package/modules/lifecycle/page-notice.js +124 -0
  160. package/modules/lifecycle/page-tweak-artist.js +18 -1
  161. package/modules/lifecycle/publish-reconciler.js +67 -20
  162. package/modules/lifecycle/routes/claims.js +5 -2
  163. package/modules/lifecycle/routes/goals.js +3 -1
  164. package/modules/lifecycle/routes/notes.js +91 -0
  165. package/modules/lifecycle/routes/project-settings.js +43 -35
  166. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  167. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  168. package/modules/lifecycle/routes/tasks.js +12 -12
  169. package/modules/lifecycle/ship-assurance.js +4 -1
  170. package/modules/lifecycle/ship-card.js +8 -2
  171. package/modules/lifecycle/task-needs-gate.js +70 -0
  172. package/modules/market-watch/CLAUDE.md +15 -0
  173. package/modules/market-watch/db.js +340 -0
  174. package/modules/market-watch/key.js +58 -0
  175. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  176. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  177. package/modules/market-watch/module.json +26 -0
  178. package/modules/market-watch/pollers/hub-seed.js +54 -0
  179. package/modules/market-watch/pollers/weekly-run.js +100 -0
  180. package/modules/market-watch/research.js +350 -0
  181. package/modules/market-watch/run.js +245 -0
  182. package/modules/market-watch/seed/market-data.json +38 -0
  183. package/modules/market-watch/seed/partners-data.json +497 -0
  184. package/modules/market-watch/seed.js +252 -0
  185. package/modules/market-watch/store.js +230 -0
  186. package/modules/memory/routes/memory.js +14 -2
  187. package/modules/memory/routes/search.js +8 -2
  188. package/modules/onboarding/howto-moments.js +8 -1
  189. package/modules/platform-identity/account-logins.js +192 -0
  190. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  191. package/modules/platform-identity/platform-identity.js +5 -4
  192. package/modules/platform-identity/routes/my-projects.js +6 -0
  193. package/modules/platform-identity/routes/profile.js +18 -0
  194. package/modules/platform-identity/routes/public-profile.js +10 -1
  195. package/modules/platform-identity/routes/sso.js +26 -1
  196. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  197. package/modules/provisioning/credential-preflight.js +13 -3
  198. package/modules/provisioning/pollers/app-liveness.js +25 -5
  199. package/modules/provisioning/routes/provisioning.js +1 -1
  200. package/modules/provisioning/starter-bundles.js +8 -2
  201. package/modules/public-landing/public/account.html +10 -2
  202. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  203. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  205. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  209. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  210. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  213. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  214. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  221. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  222. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  223. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  224. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  225. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  226. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  227. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  228. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  229. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  230. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  233. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  244. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  245. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  246. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  247. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  248. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  249. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  250. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  251. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  252. package/modules/public-landing/public/community.html +1 -1
  253. package/modules/public-landing/public/connections.html +2 -2
  254. package/modules/public-landing/public/guild.html +1 -1
  255. package/modules/public-landing/public/invitations.html +2 -2
  256. package/modules/public-landing/public/projects.html +5 -5
  257. package/modules/raise/views/nda.html +2 -0
  258. package/modules/specialities/db.js +32 -1
  259. package/modules/specialities/routes/specialities.js +10 -6
  260. package/modules/status-ui/public/index.html +1 -1
  261. package/package-lock.json +2 -2
  262. package/package.json +1 -1
  263. package/release-notes.json +286 -0
  264. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  265. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  266. package/scripts/bongos/claim.js +22 -1
  267. package/scripts/bongos/db-backup-nightly.sh +29 -0
  268. package/scripts/bongos/deploy-divergence.js +7 -0
  269. package/scripts/bongos/doc-cli-guard.js +6 -0
  270. package/scripts/bongos/fitness.js +3 -2
  271. package/scripts/bongos/gen-diagrams.js +3 -3
  272. package/scripts/bongos/lib.sh +4 -5
  273. package/scripts/bongos/migration-namespace.js +13 -1
  274. package/scripts/bongos/plain-cards.js +4 -0
  275. package/scripts/bongos/provision-github-setup.js +3 -1
  276. package/scripts/bongos/provision-units.js +6 -1
  277. package/scripts/bongos/publish-manifest.js +4 -0
  278. package/scripts/bongos/release.js +2 -0
  279. package/scripts/bongos/run-unit-tests.js +268 -82
  280. package/scripts/bongos/seed-tasks.example.json +56 -0
  281. package/scripts/bongos/seed-tasks.js +222 -0
  282. package/scripts/bongos/seed-verify-criteria.js +1 -2
  283. package/scripts/bongos/ship-finish.js +2 -0
  284. package/scripts/bongos/ship-resume.js +2 -0
  285. package/scripts/bongos/strand-lease.js +250 -0
  286. package/scripts/bongos/strand-watch.js +129 -12
  287. package/scripts/bongos/version-literal-guard.js +2 -2
  288. package/scripts/discord/post.js +4 -3
  289. package/scripts/gds/seed-tasks.js +7 -0
  290. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  291. package/scripts/hall-preview/server.js +21 -0
  292. package/src/bongos/auth-config.js +29 -1
  293. package/src/bongos/auth-discord.js +3 -3
  294. package/src/bongos/auth-github.js +3 -6
  295. package/src/bongos/auth-google.js +217 -0
  296. package/src/bongos/device-approval.js +140 -0
  297. package/src/bongos/llm-pricing.js +17 -1
  298. package/src/bongos/metrics.js +48 -2
  299. package/src/bongos/middleware/rate-limit.js +30 -11
  300. package/src/bongos/module-scope-map.js +16 -1
  301. package/src/bongos/optimizer.js +15 -1
  302. package/src/bongos/payout-approvals.js +342 -0
  303. package/src/bongos/payout-gates.js +364 -0
  304. package/src/bongos/prelaunch-gate.js +7 -9
  305. package/src/bongos/prelaunch-switch.js +20 -0
  306. package/src/bongos/route-rank-check.js +3 -0
  307. package/src/bongos/routes/auth-google.js +246 -0
  308. package/src/bongos/routes/auth.js +38 -1
  309. package/src/bongos/routes/backup.js +33 -9
  310. package/src/bongos/routes/builders.js +57 -30
  311. package/src/bongos/routes/github.js +9 -0
  312. package/src/bongos/routes/me.js +18 -0
  313. package/src/bongos/routes/my-payouts.js +133 -0
  314. package/src/bongos/routes/payout-approvals.js +312 -0
  315. package/src/bongos/routes/payout-gates.js +179 -0
  316. package/src/bongos/routes.js +15 -1
  317. package/src/bongos/secret-box.js +18 -3
  318. package/src/bongos/serve-internal.js +47 -19
  319. package/src/bongos/sign-in-pages.js +138 -0
  320. package/src/bongos/task-needs.js +146 -0
  321. package/src/bongos-downloads.js +18 -1
  322. package/src/instance-config.js +28 -0
  323. package/src/module-api.js +14 -1
  324. package/src/platform-server.js +27 -1
  325. package/tests/account_logins.mjs +326 -0
  326. package/tests/account_logins_db.mjs +161 -0
  327. package/tests/ai_studio_billing.mjs +253 -0
  328. package/tests/ai_studio_generate.mjs +282 -0
  329. package/tests/ai_studio_keys.mjs +159 -0
  330. package/tests/ai_studio_requests.mjs +319 -0
  331. package/tests/auth_backchannel_logout_route.mjs +201 -0
  332. package/tests/auto_satisfy_criteria.mjs +10 -1
  333. package/tests/autobongos_tweak_lane.mjs +144 -0
  334. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  335. package/tests/backfill_subagent_rewards.mjs +17 -1
  336. package/tests/backup_trigger_lock.mjs +119 -0
  337. package/tests/batched_writes_parity.mjs +340 -0
  338. package/tests/blockers_people.mjs +198 -0
  339. package/tests/bounded_rate_maps.mjs +184 -0
  340. package/tests/claimable_blocker_status.mjs +1 -1
  341. package/tests/claims_batch_routes.mjs +421 -0
  342. package/tests/conductor_main_e2e.mjs +4 -0
  343. package/tests/config_read_cache.mjs +171 -0
  344. package/tests/copy_desk_page_editor.mjs +12 -0
  345. package/tests/cost_write_guard.mjs +200 -0
  346. package/tests/craft_achievements.mjs +33 -10
  347. package/tests/criterion_goal_attach.mjs +1 -1
  348. package/tests/db_backup_nightly.mjs +73 -1
  349. package/tests/deploy_divergence_line.mjs +7 -0
  350. package/tests/effective_visibility_predicate.mjs +12 -0
  351. package/tests/fitness.mjs +19 -0
  352. package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
  353. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  354. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  355. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  356. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  357. package/tests/github_one_trip.mjs +34 -0
  358. package/tests/github_repo_visibility.mjs +20 -0
  359. package/tests/goal_achievement.mjs +30 -1
  360. package/tests/goal_achievement_reward.mjs +205 -0
  361. package/tests/goal_archive_disposition.mjs +5 -5
  362. package/tests/goal_sort_staleness.mjs +2 -1
  363. package/tests/goal_tier.mjs +11 -1
  364. package/tests/google_fonts_nonblocking.mjs +84 -0
  365. package/tests/google_sign_in.mjs +401 -0
  366. package/tests/government_board_expiry.mjs +94 -0
  367. package/tests/government_protected_surfaces.mjs +1 -1
  368. package/tests/government_routes.mjs +146 -23
  369. package/tests/grader.mjs +31 -0
  370. package/tests/hall_ai_studio.mjs +347 -0
  371. package/tests/hall_city.mjs +8 -0
  372. package/tests/hall_page_gate_map.mjs +3 -0
  373. package/tests/hall_studio_home.mjs +61 -2
  374. package/tests/home_attention_cards.mjs +1 -1
  375. package/tests/hook_shell_tools.mjs +8 -0
  376. package/tests/hooks_drain.mjs +1 -1
  377. package/tests/host_topology_skips.mjs +2 -0
  378. package/tests/howto_moments.mjs +17 -0
  379. package/tests/human_signal.mjs +141 -0
  380. package/tests/install_script_reader.mjs +51 -0
  381. package/tests/integration_fixture_ids.mjs +82 -0
  382. package/tests/invite_expiry_db.mjs +13 -1
  383. package/tests/lib_sh_resolution.mjs +0 -4
  384. package/tests/market_watch_data.mjs +244 -0
  385. package/tests/market_watch_data_db.mjs +308 -0
  386. package/tests/market_watch_module.mjs +114 -0
  387. package/tests/market_watch_run.mjs +503 -0
  388. package/tests/metrics.mjs +124 -0
  389. package/tests/migration_namespace.mjs +14 -0
  390. package/tests/module_api.mjs +2 -0
  391. package/tests/module_loader.mjs +1 -1
  392. package/tests/module_skills_hook.mjs +69 -0
  393. package/tests/notes.mjs +182 -0
  394. package/tests/own_scope_guard.mjs +5 -0
  395. package/tests/page_tweak_marker_sql.mjs +76 -0
  396. package/tests/page_tweak_no_block.mjs +135 -0
  397. package/tests/page_tweak_notice.mjs +103 -0
  398. package/tests/payout_approvals.mjs +253 -0
  399. package/tests/payout_approvals_db.mjs +106 -0
  400. package/tests/payout_gates.mjs +322 -0
  401. package/tests/payout_gates_db.mjs +113 -0
  402. package/tests/planning_session_skill.mjs +13 -3
  403. package/tests/project_settings_page.mjs +29 -5
  404. package/tests/project_settings_spend_cap_route.mjs +235 -0
  405. package/tests/promotion_sweep.mjs +29 -1
  406. package/tests/provision.mjs +3 -2
  407. package/tests/provisioning_app_status.mjs +18 -0
  408. package/tests/provisioning_credential_preflight.mjs +53 -0
  409. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  410. package/tests/public_landing_projects.mjs +37 -0
  411. package/tests/publish_reconciler_overlap.mjs +60 -0
  412. package/tests/rank_change_broadcast_port.mjs +151 -0
  413. package/tests/seed_tasks.mjs +176 -0
  414. package/tests/seed_verify_criteria.mjs +2 -6
  415. package/tests/session_start_freshness.mjs +66 -1
  416. package/tests/shell_hooks.mjs +209 -0
  417. package/tests/ship_assurance.mjs +28 -0
  418. package/tests/ship_card.mjs +20 -5
  419. package/tests/site_classified_once.mjs +142 -0
  420. package/tests/speciality_db_shape.mjs +23 -0
  421. package/tests/speciality_session_skills.mjs +48 -2
  422. package/tests/strand_lease.mjs +249 -0
  423. package/tests/strand_watch.mjs +58 -0
  424. package/tests/task_criteria_routes.mjs +275 -0
  425. package/tests/task_criteria_unlink_db.mjs +123 -0
  426. package/tests/task_needs.mjs +233 -0
  427. package/tests/task_needs_db.mjs +110 -0
  428. package/tests/unit_runner_serial.mjs +266 -43
  429. package/tests/version_close_route.mjs +1 -1
  430. package/tests/version_literals.mjs +5 -7
  431. package/tests/welcome_boot.mjs +16 -5
  432. package/tests/welcome_boot_browser.mjs +36 -4
  433. package/docs/recipes/bongos-cli-release.md +0 -66
  434. package/scripts/bongos/backfill-disciplines.js +0 -144
  435. package/scripts/bongos/backfill-session-earnings.js +0 -123
  436. package/scripts/bongos/backfill-session-types.js +0 -87
  437. package/scripts/bongos/build-bongos.js +0 -94
  438. package/scripts/bongos/cleanup-orphans.js +0 -74
  439. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  440. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  441. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  442. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  443. package/scripts/bongos/smoke-batch-release.sh +0 -316
  444. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  445. package/scripts/bongos/smoke-dependencies.sh +0 -143
  446. package/scripts/gds/backfill-disciplines.js +0 -7
  447. package/scripts/gds/backfill-session-earnings.js +0 -7
  448. package/scripts/gds/backfill-session-types.js +0 -7
  449. package/scripts/gds/cleanup-orphans.js +0 -7
  450. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  451. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  452. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  453. package/scripts/gds/smoke-batch-claim.sh +0 -8
  454. package/scripts/gds/smoke-batch-release.sh +0 -8
  455. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  456. package/scripts/gds/smoke-dependencies.sh +0 -8
  457. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,123 @@
1
+ // tests/task_criteria_unlink_db.mjs — what unlinking an OPEN task from a criterion
2
+ // does to that criterion's close (task 1004984, gap 4). REAL DB: the close is one
3
+ // UPDATE ... FROM candidate statement whose grouping, FILTER counts and sign-off
4
+ // recency clause only Postgres evaluates.
5
+ //
6
+ // The scenario. One criterion gates two tasks: one SHIPPED, one still OPEN. A
7
+ // current backend_signoff sits on the criterion (recorded after the shipped task
8
+ // landed). While the open task is linked, the delivery guard holds the criterion
9
+ // open. Then the open task's link is removed (removeTaskCriterion, the DELETE
10
+ // route's write) and autoSatisfyShippedCriteria runs.
11
+ //
12
+ // The recorded outcome (task 1004984's ship notes): the criterion CLOSES, on the
13
+ // sign-off it already had, attributed to the shipped task. The sign-off is current
14
+ // by the reconciler's rule — no linked task shipped after it — so this is the
15
+ // intended rule, not a close without a fresh sign-off. The goal does NOT achieve,
16
+ // because the unlinked task is still an unfinished task of the goal. A second
17
+ // criterion with the same links and NO sign-off stays open: unlinking alone never
18
+ // closes a criterion.
19
+ //
20
+ // Real-DB integration test (self-skips without a reachable Postgres; in the
21
+ // INTEGRATION set so the default DB-free lane never runs it).
22
+ //
23
+ // Run: DATABASE_URL=... node tests/task_criteria_unlink_db.mjs
24
+
25
+ import { createRequire } from 'node:module';
26
+ import { strict as assert } from 'node:assert';
27
+ const require = createRequire(import.meta.url);
28
+ const { Pool } = require('pg');
29
+
30
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
31
+ try {
32
+ await pool.query('SELECT 1');
33
+ } catch (e) {
34
+ console.log(`task_criteria_unlink_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
35
+ await pool.end().catch(() => {});
36
+ process.exit(0);
37
+ }
38
+
39
+ const seams = require('../src/module-seams.js');
40
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
41
+ const db = require('../modules/lifecycle/db.js');
42
+ const { autoSatisfyShippedCriteria } = require('../modules/lifecycle/done-when.js');
43
+
44
+ const VER = 'ZZZ-CRIT-UNLINK', SIGNER = 90000411;
45
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
46
+
47
+ async function cleanup() {
48
+ const ids = (await q(`SELECT id FROM tasks WHERE version_id=$1`, [VER]).catch(() => [])).map((r) => r.id);
49
+ const crits = (await q(`SELECT id FROM done_when_criteria WHERE version_id=$1`, [VER]).catch(() => [])).map((r) => r.id);
50
+ await q(`DELETE FROM lifecycle_criterion_uats WHERE criterion_id = ANY($1::bigint[])`, [crits]).catch(() => {});
51
+ await q(`DELETE FROM task_criteria WHERE task_id = ANY($1::bigint[]) OR criterion_id = ANY($2::bigint[])`, [ids, crits]).catch(() => {});
52
+ await q(`UPDATE done_when_criteria SET satisfied_by_task_id = NULL WHERE version_id=$1`, [VER]).catch(() => {});
53
+ await q(`DELETE FROM dependencies WHERE (from_kind='task' AND from_id = ANY($1::bigint[])) OR (to_kind='task' AND to_id = ANY($1::bigint[]))`, [ids]).catch(() => {});
54
+ await q(`DELETE FROM done_when_criteria WHERE version_id=$1`, [VER]).catch(() => {});
55
+ await q(`DELETE FROM tasks WHERE version_id=$1`, [VER]).catch(() => {});
56
+ await q(`DELETE FROM goal_members WHERE goal_id IN (SELECT id FROM goals WHERE version_id=$1)`, [VER]).catch(() => {});
57
+ await q(`DELETE FROM goals WHERE version_id=$1`, [VER]).catch(() => {});
58
+ await q(`DELETE FROM builders WHERE id=$1`, [SIGNER]).catch(() => {});
59
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
60
+ }
61
+
62
+ let pass = 0;
63
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
64
+ const crit = async (id) => (await q(`SELECT satisfied, satisfied_by_task_id FROM done_when_criteria WHERE id=$1`, [id]))[0];
65
+ const goalStatus = async (id) => (await q(`SELECT status FROM goals WHERE id=$1`, [id]))[0].status;
66
+
67
+ try {
68
+ await cleanup();
69
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'unlink','building','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
70
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'unlink-s','unlink-signer','Unlink Signer','archon')`, [SIGNER]);
71
+
72
+ const goal = await db.createGoal({ versionId: VER, title: 'Unlink goal', createdBy: SIGNER });
73
+ const shipped = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The shipped half', kind: 'feature', discipline: 'engineer', creditsReward: 10 });
74
+ const open = await db.createTask({ versionId: VER, goalId: goal.id, title: 'The open half', kind: 'feature', discipline: 'engineer', creditsReward: 10 });
75
+ await q(`UPDATE tasks SET status='shipped', shipped_at = now() - interval '1 hour' WHERE id=$1`, [shipped.id]);
76
+
77
+ // Two criteria, the same two links on each. Only the first carries a sign-off.
78
+ const [signed] = await q(
79
+ `INSERT INTO done_when_criteria (version_id, goal_id, criterion_id, criterion_md, sort_order)
80
+ VALUES ($1, $2, 'unlink-signed', 'Signed criterion', 1) RETURNING id`, [VER, goal.id]);
81
+ const [unsigned] = await q(
82
+ `INSERT INTO done_when_criteria (version_id, goal_id, criterion_id, criterion_md, sort_order)
83
+ VALUES ($1, $2, 'unlink-unsigned', 'Unsigned criterion', 2) RETURNING id`, [VER, goal.id]);
84
+ for (const c of [signed.id, unsigned.id]) {
85
+ await db.addTaskCriterion(shipped.id, c, 'manual');
86
+ await db.addTaskCriterion(open.id, c, 'manual');
87
+ }
88
+ // A current backend sign-off: created now, after the shipped task's shipped_at.
89
+ await q(
90
+ `INSERT INTO lifecycle_criterion_uats (criterion_id, kind, signed_off_by, live_basis, note)
91
+ VALUES ($1, 'backend_signoff', $2, 'attested', 'signed while the open task was linked')`,
92
+ [signed.id, SIGNER]);
93
+
94
+ // 1) While the open task is linked, the delivery guard holds BOTH criteria open.
95
+ for (const c of [signed.id, unsigned.id]) await autoSatisfyShippedCriteria(pool, { criterionId: c });
96
+ ok((await crit(signed.id)).satisfied === false, 'a linked open task holds the signed criterion open');
97
+ ok((await crit(unsigned.id)).satisfied === false, 'a linked open task holds the unsigned criterion open');
98
+
99
+ // 2) Unlink the open task from both (the DELETE route's write).
100
+ ok(await db.removeTaskCriterion(open.id, signed.id), 'the open task\'s link to the signed criterion is removed');
101
+ ok(await db.removeTaskCriterion(open.id, unsigned.id), 'the open task\'s link to the unsigned criterion is removed');
102
+ ok((await db.removeTaskCriterion(open.id, signed.id)) === false, 'a second unlink removes nothing and says so');
103
+
104
+ // 3) Run the reconciler's close, scoped per criterion so no other test's data is swept.
105
+ const outSigned = await autoSatisfyShippedCriteria(pool, { criterionId: signed.id });
106
+ const outUnsigned = await autoSatisfyShippedCriteria(pool, { criterionId: unsigned.id });
107
+
108
+ const s = await crit(signed.id);
109
+ ok(s.satisfied === true, 'OUTCOME: the signed criterion closes once only the shipped task is linked');
110
+ ok(Number(s.satisfied_by_task_id) === Number(shipped.id), 'the close is attributed to the shipped task');
111
+ ok(outSigned.criteria.length === 1, 'the call reports exactly that one criterion closed');
112
+
113
+ ok((await crit(unsigned.id)).satisfied === false, 'with no sign-off, unlinking alone never closes a criterion');
114
+ ok(outUnsigned.criteria.length === 0, 'and the call reports nothing closed');
115
+
116
+ ok((await goalStatus(goal.id)) === 'open', 'the goal stays open — the unlinked task is still unfinished work of the goal');
117
+ ok(outSigned.goals.length === 0, 'no goal was achieved by the close');
118
+
119
+ console.log(`\ntask_criteria_unlink_db.mjs: ${pass} passed`);
120
+ } finally {
121
+ await cleanup();
122
+ await pool.end();
123
+ }
@@ -0,0 +1,233 @@
1
+ // tests/task_needs.mjs — a task says what it needs; each person's lists show only
2
+ // what they can do (task 1004950, ADR 0395). DB-free: the real feed
3
+ // (listClaimableTasks), the real claim gate (taskNeedsFailures) and the real
4
+ // capability read (resolveCapabilities) run against a stand-in pool that answers
5
+ // each query from fixtures — and the feed's stand-in applies the predicate the
6
+ // SQL names (`needs <@ capabilities`) to the parameter the code actually bound,
7
+ // so a feed that bound the wrong capabilities, or none, fails here.
8
+ // tests/task_needs_db.mjs proves the same rows against real Postgres.
9
+ //
10
+ // Run: node tests/task_needs.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import { createRequire } from 'node:module';
14
+ import { readFileSync } from 'node:fs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const api = require('../src/module-api.js');
18
+ const seams = require('../src/module-seams.js');
19
+ const { setCapabilityGrants } = require('../src/bongos/task-needs.js');
20
+ const needs = { ...api.taskNeeds, setCapabilityGrants };
21
+ const gate = require('../modules/lifecycle/task-needs-gate.js');
22
+ const db = require('../modules/lifecycle/db.js');
23
+
24
+ let passed = 0, failed = 0;
25
+ async function t(name, fn) {
26
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
27
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
28
+ }
29
+
30
+ // ---- fixtures: four people and four tasks --------------------------------
31
+ const OWNER = 7, GRANTED = 8, PLAIN = 9, FOUNDER = 3;
32
+ const GRANTS = { [OWNER]: [], [GRANTED]: ['server'], [PLAIN]: [], [FOUNDER]: [] };
33
+ const TASKS = [
34
+ { id: 101, needs: [] },
35
+ { id: 102, needs: ['owner'] },
36
+ { id: 103, needs: ['server'] },
37
+ { id: 104, needs: ['owner', 'server'] },
38
+ ];
39
+ const seatHolder = { id: OWNER };
40
+ const governmentPort = { positionsFor: async (id) => ({ held: String(id) === String(seatHolder.id) ? [{ kind: 'owner' }] : [] }) };
41
+
42
+ const subset = (a, b) => a.every((x) => b.includes(x));
43
+ let lastFeed = null;
44
+ const realQuery = api.pool.query;
45
+ api.pool.query = async (sql, params = []) => {
46
+ if (/SELECT capability_grants FROM builders/.test(sql)) return { rows: [{ capability_grants: GRANTS[params[0]] || [] }] };
47
+ if (/FROM builders WHERE system_role IS NULL ORDER BY id/.test(sql)) return { rows: [{ id: String(FOUNDER) }] };
48
+ if (/SELECT ct\.\*, sf\.fits_session_modes/.test(sql)) { // the session optimizer
49
+ const m = sql.match(/COALESCE\(ct\.needs, '\{\}'::text\[\]\) <@ \$(\d+)::text\[\]/);
50
+ lastFeed = { sql, params, capsParam: m ? params[Number(m[1]) - 1] : undefined };
51
+ const caps = m ? lastFeed.capsParam : ['owner', 'server'];
52
+ return { rows: TASKS.filter((r) => subset(r.needs, caps)).map((r) => ({ ...r, touches: [`f${r.id}`], est_minutes: 10 })) };
53
+ }
54
+ if (/FROM \(\s*SELECT t\.\*, pms_calibrated_minutes/.test(sql)) {
55
+ const m = sql.match(/COALESCE\(v\.needs, '\{\}'::text\[\]\) <@ \$(\d+)::text\[\]/);
56
+ lastFeed = { sql, params, capsParam: m ? params[Number(m[1]) - 1] : undefined };
57
+ if (!m) return { rows: TASKS.map((r) => ({ ...r })) }; // no clause bound → nothing narrows
58
+ const caps = lastFeed.capsParam;
59
+ const oneNeed = sql.match(/v\.needs @> ARRAY\[\$(\d+)\]::text\[\]/);
60
+ const none = /cardinality\(v\.needs\) = 0/.test(sql);
61
+ return {
62
+ rows: TASKS
63
+ .filter((r) => subset(r.needs, caps))
64
+ .filter((r) => (none ? r.needs.length === 0 : true))
65
+ .filter((r) => (oneNeed ? r.needs.includes(params[Number(oneNeed[1]) - 1]) : true))
66
+ .map((r) => ({ ...r })),
67
+ };
68
+ }
69
+ throw new Error(`unexpected query: ${sql.slice(0, 80)}`);
70
+ };
71
+ if (seams.hasProvider && seams.hasProvider('government')) throw new Error('test expects no government provider');
72
+ seams.registerProvider('government', governmentPort);
73
+
74
+ const feedIds = async (builderId, opts = {}) =>
75
+ (await db.listClaimableTasks({ builderId, ...opts })).map((r) => Number(r.id)).sort();
76
+
77
+ // ---- the vocabulary --------------------------------------------------------
78
+ await t('normalizeNeeds accepts arrays, comma lists and nothing; refuses unknown words', () => {
79
+ assert.deepEqual(needs.normalizeNeeds(['server', 'owner', 'server']).needs, ['owner', 'server']);
80
+ assert.deepEqual(needs.normalizeNeeds('server, owner').needs, ['owner', 'server']);
81
+ assert.deepEqual(needs.normalizeNeeds(null).needs, []);
82
+ assert.deepEqual(needs.normalizeNeeds([]).needs, []);
83
+ assert.deepEqual(needs.normalizeNeeds(['dns']), { ok: false, bad: ['dns'] });
84
+ assert.equal(needs.normalizeNeeds(5).ok, false);
85
+ });
86
+
87
+ await t('the migration CHECK and the code list name the same needs', () => {
88
+ const mig = readFileSync(new URL('../migrations/core_285_task_needs.sql', import.meta.url), 'utf8');
89
+ const tasksCheck = mig.match(/needs <@ ARRAY\[([^\]]+)\]/)[1].match(/'([a-z]+)'/g).map((s) => s.slice(1, -1));
90
+ assert.deepEqual(tasksCheck, [...needs.TASK_NEEDS]);
91
+ const grantCheck = mig.match(/capability_grants <@ ARRAY\[([^\]]+)\]/)[1].match(/'([a-z]+)'/g).map((s) => s.slice(1, -1));
92
+ assert.deepEqual(grantCheck, [...needs.GRANTABLE_CAPABILITIES]);
93
+ });
94
+
95
+ await t('only server refuses a claim; owner never does', () => {
96
+ assert.deepEqual(needs.claimRefusalNeeds(['owner'], []), []);
97
+ assert.deepEqual(needs.claimRefusalNeeds(['server'], []), ['server']);
98
+ assert.deepEqual(needs.claimRefusalNeeds(['owner', 'server'], ['owner']), ['server']);
99
+ assert.deepEqual(needs.claimRefusalNeeds(['server'], ['server']), []);
100
+ });
101
+
102
+ // ---- capabilities are read, never asserted ---------------------------------
103
+ await t('the owner seat gives owner; a grant gives server; a plain builder has neither', async () => {
104
+ assert.deepEqual(await needs.resolveCapabilities(OWNER), ['owner']);
105
+ assert.deepEqual(await needs.resolveCapabilities(GRANTED), ['server']);
106
+ assert.deepEqual(await needs.resolveCapabilities(PLAIN), []);
107
+ });
108
+
109
+ await t('with no seat held, the founding builder is the owner (the UAT sign-off fallback)', async () => {
110
+ assert.deepEqual(await needs.resolveCapabilities(FOUNDER, { government: null }), ['owner']);
111
+ assert.deepEqual(await needs.resolveCapabilities(OWNER, { government: null }), []);
112
+ const broken = { positionsFor: async () => { throw new Error('down'); } };
113
+ assert.deepEqual(await needs.resolveCapabilities(FOUNDER, { government: broken }), ['owner'], 'a failed seat read falls back, never widens');
114
+ });
115
+
116
+ // ---- the feed --------------------------------------------------------------
117
+ await t('a builder without server access sees no server task, and no owner task', async () => {
118
+ assert.deepEqual(await feedIds(PLAIN), [101]);
119
+ assert.deepEqual(lastFeed.capsParam, [], 'the capabilities bound into the query are the viewer\'s');
120
+ });
121
+
122
+ await t('the owner sees owner tasks in their own list, but not server work', async () => {
123
+ assert.deepEqual(await feedIds(OWNER), [101, 102]);
124
+ });
125
+
126
+ await t('a builder granted server access sees server tasks', async () => {
127
+ assert.deepEqual(await feedIds(GRANTED), [101, 103]);
128
+ });
129
+
130
+ await t('the owner holding a server grant sees everything', async () => {
131
+ GRANTS[OWNER] = ['server'];
132
+ try { assert.deepEqual(await feedIds(OWNER), [101, 102, 103, 104]); }
133
+ finally { GRANTS[OWNER] = []; }
134
+ });
135
+
136
+ await t('the autonomous runner never holds owner, whoever it runs as', async () => {
137
+ assert.deepEqual(await feedIds(OWNER, { fit: 'autonomous' }), [101]);
138
+ assert.deepEqual(lastFeed.capsParam, []);
139
+ });
140
+
141
+ await t('?needs=owner is the owner\'s "waiting on you" queue', async () => {
142
+ assert.deepEqual(await feedIds(OWNER, { needs: 'owner' }), [102]);
143
+ assert.deepEqual(await feedIds(PLAIN, { needs: 'owner' }), [], 'it never widens what a viewer is shown');
144
+ assert.deepEqual(await feedIds(OWNER, { needs: 'none' }), [101]);
145
+ });
146
+
147
+ await t('the board filter on GET /tasks narrows by need without the capability rule', () => {
148
+ const params = [];
149
+ assert.equal(gate.needsFilterWhere('server', '', params), 'needs @> ARRAY[$1]::text[]');
150
+ assert.deepEqual(params, ['server']);
151
+ assert.equal(gate.needsFilterWhere('none', '', []), 'cardinality(needs) = 0');
152
+ assert.equal(gate.needsFilterWhere(undefined, '', []), null);
153
+ });
154
+
155
+ // ---- the session optimizer (the other recommend list) -----------------------
156
+ const optimizer = require('../src/bongos/optimizer.js');
157
+ const planIds = async (builderId, mode) =>
158
+ [...(await optimizer.optimizeSession({ builderId, mode, timeBudgetMin: 600, maxParallel: 10 })).recommended.map((r) => Number(r.id))].sort();
159
+
160
+ await t('the session plan applies the same rule, per capability', async () => {
161
+ assert.deepEqual(await planIds(PLAIN, 'active'), [101]);
162
+ assert.deepEqual(await planIds(OWNER, 'active'), [101, 102]);
163
+ assert.deepEqual(await planIds(GRANTED, 'active'), [101, 103]);
164
+ });
165
+
166
+ await t('an autonomous session plan never holds owner, even for the owner', async () => {
167
+ assert.deepEqual(await planIds(OWNER, 'autonomous'), [101]);
168
+ assert.deepEqual(lastFeed.capsParam, []);
169
+ });
170
+
171
+ // ---- the claim gate --------------------------------------------------------
172
+ const fakeClient = {
173
+ query: async (sql, params) => {
174
+ assert.match(sql, /SELECT id, needs FROM tasks WHERE id = ANY/);
175
+ return { rows: TASKS.filter((r) => params[0].includes(r.id) && r.needs.length > 0) };
176
+ },
177
+ };
178
+
179
+ await t('a server task refuses a claimer without the grant, in plain words', async () => {
180
+ const f = await gate.taskNeedsFailures({ client: fakeClient, tasks: [{ id: 103 }], builderId: PLAIN });
181
+ assert.equal(f.length, 1);
182
+ assert.equal(f[0].refusal.code, 'NEEDS_SERVER_ACCESS');
183
+ assert.match(f[0].refusal.message, /server access/);
184
+ assert.doesNotMatch(f[0].refusal.message, /capabilit|grant|NEEDS_/, 'no machinery in the sentence a person reads');
185
+ });
186
+
187
+ await t('the same server task is claimable with the grant', async () => {
188
+ assert.deepEqual(await gate.taskNeedsFailures({ client: fakeClient, tasks: [{ id: 103 }, { id: 104 }], builderId: GRANTED }), []);
189
+ });
190
+
191
+ await t('owner work is claimable — by the owner and, if they reach it, by anyone', async () => {
192
+ assert.deepEqual(await gate.taskNeedsFailures({ client: fakeClient, tasks: [{ id: 102 }], builderId: OWNER }), []);
193
+ assert.deepEqual(await gate.taskNeedsFailures({ client: fakeClient, tasks: [{ id: 102 }], builderId: PLAIN }), []);
194
+ });
195
+
196
+ await t('a task needing owner AND server refuses the owner without a grant (server is the refusal)', async () => {
197
+ const f = await gate.taskNeedsFailures({ client: fakeClient, tasks: [{ id: 104 }], builderId: OWNER });
198
+ assert.deepEqual(f.map((x) => x.refusal.needs), [['server']]);
199
+ });
200
+
201
+ await t('all three claim surfaces call the gate', () => {
202
+ const src = readFileSync(new URL('../modules/lifecycle/db-claims.js', import.meta.url), 'utf8');
203
+ assert.equal((src.match(/await taskNeedsFailures\(/g) || []).length, 3);
204
+ });
205
+
206
+ // ---- granting server access ------------------------------------------------
207
+ await t('the module doorway publishes the reads, never the grant write', () => {
208
+ assert.equal(typeof api.taskNeeds.resolveCapabilities, 'function');
209
+ assert.equal(api.taskNeeds.setCapabilityGrants, undefined, 'a module must not be able to grant server access');
210
+ });
211
+
212
+ await t('only the owner seat can grant server access — checked at the write itself', async () => {
213
+ const writes = [];
214
+ const grantPool = {
215
+ query: async (sql, params) => {
216
+ if (/SELECT capability_grants FROM builders/.test(sql)) return { rows: [{ capability_grants: [] }] };
217
+ if (/SELECT id, system_role FROM builders/.test(sql)) return { rows: [{ id: params[0], system_role: null }] };
218
+ if (/UPDATE builders SET capability_grants/.test(sql)) { writes.push(params); return { rows: [{ id: params[0], capability_grants: params[1] }] }; }
219
+ throw new Error(`unexpected: ${sql.slice(0, 60)}`);
220
+ },
221
+ };
222
+ const deps = { pool: grantPool, government: governmentPort, getFoundingBuilder: async () => ({ id: FOUNDER }) };
223
+ let refused = null;
224
+ try { await needs.setCapabilityGrants({ actorBuilderId: PLAIN, builderId: PLAIN, grants: ['server'] }, deps); } catch (e) { refused = e; }
225
+ assert.equal(refused && refused.code, 'NOT_PROJECT_OWNER');
226
+ assert.equal(writes.length, 0, 'nothing written for a non-owner');
227
+ const row = await needs.setCapabilityGrants({ actorBuilderId: OWNER, builderId: PLAIN, grants: ['server', 'owner'] }, deps);
228
+ assert.deepEqual(row.capability_grants, ['server'], 'owner is never granted by hand');
229
+ });
230
+
231
+ api.pool.query = realQuery;
232
+ console.log(`\ntask_needs (task 1004950): ${passed} passed, ${failed} failed`);
233
+ if (failed) process.exitCode = 1;
@@ -0,0 +1,110 @@
1
+ // tests/task_needs_db.mjs — task 1004950 (ADR 0395), real DB.
2
+ //
3
+ // What a task needs, against real Postgres: core_285's columns and CHECKs, the
4
+ // real claimable feed and the real claim gate on all three surfaces, each driven
5
+ // with and without each capability. tests/task_needs.mjs covers the same rules
6
+ // DB-free; this is the proof the SQL says what the stand-in assumed.
7
+ // Real-DB integration test (self-skips without a reachable Postgres; in the
8
+ // INTEGRATION set so the default DB-free lane never runs it).
9
+ //
10
+ // Run: DATABASE_URL=... node tests/task_needs_db.mjs
11
+
12
+ import { createRequire } from 'node:module';
13
+ import { strict as assert } from 'node:assert';
14
+ const require = createRequire(import.meta.url);
15
+ const { Pool } = require('pg');
16
+
17
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
18
+ try {
19
+ await pool.query('SELECT 1');
20
+ } catch (e) {
21
+ console.log(`task_needs_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
22
+ await pool.end().catch(() => {});
23
+ process.exit(0);
24
+ }
25
+
26
+ const seams = require('../src/module-seams.js');
27
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
28
+ const db = require('../modules/lifecycle/db.js');
29
+
30
+ const VER = 'ZZZ-TN';
31
+ const OWNER = 90000901, GRANTED = 90000902, PLAIN = 90000903;
32
+ const T_NONE = 90000911, T_OWNER = 90000912, T_SERVER = 90000913;
33
+ const BUILDERS = [OWNER, GRANTED, PLAIN];
34
+ const TASKS = [T_NONE, T_OWNER, T_SERVER];
35
+ // The owner seat, as the government port answers it — only OWNER holds it here.
36
+ if (!seams.hasProvider('government')) {
37
+ seams.registerProvider('government', { positionsFor: async (id) => ({ held: Number(id) === OWNER ? [{ kind: 'owner' }] : [] }) });
38
+ }
39
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
40
+
41
+ async function cleanup() {
42
+ await q(`DELETE FROM credit_log WHERE builder_id = ANY($1::bigint[]) OR task_id = ANY($2::bigint[])`, [BUILDERS, TASKS]).catch(() => {});
43
+ await q(`DELETE FROM claims WHERE task_id = ANY($1::bigint[])`, [TASKS]).catch(() => {});
44
+ await q(`DELETE FROM tasks WHERE id = ANY($1::bigint[])`, [TASKS]).catch(() => {});
45
+ await q(`DELETE FROM builders WHERE id = ANY($1::bigint[])`, [BUILDERS]).catch(() => {});
46
+ await q(`DELETE FROM versions WHERE id=$1`, [VER]).catch(() => {});
47
+ }
48
+
49
+ let pass = 0;
50
+ const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
51
+ const ours = (rows) => rows.map((r) => Number(r.id)).filter((id) => TASKS.includes(id)).sort();
52
+
53
+ try {
54
+ await cleanup();
55
+ await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'tn','building','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
56
+ for (const [id, grants] of [[OWNER, '{}'], [GRANTED, '{server}'], [PLAIN, '{}']]) {
57
+ await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank,capability_grants) VALUES ($1,$2,$3,$3,'metic',$4::text[])`,
58
+ [id, `tn-${id}`, `tn-${id}`, grants]);
59
+ }
60
+ for (const [id, needs] of [[T_NONE, '{}'], [T_OWNER, '{owner}'], [T_SERVER, '{server}']]) {
61
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,needs) VALUES ($1,$2,$3,'ready',10,'feature','engineer',$4::text[])`,
62
+ [id, VER, `tn ${id}`, needs]);
63
+ }
64
+
65
+ // The columns refuse a need nobody knows.
66
+ let bad = null;
67
+ try { await q(`UPDATE tasks SET needs='{dns}' WHERE id=$1`, [T_NONE]); } catch (e) { bad = e; }
68
+ ok(bad && bad.code === '23514', 'tasks.needs refuses an unknown need (CHECK)');
69
+ bad = null;
70
+ try { await q(`UPDATE builders SET capability_grants='{owner}' WHERE id=$1`, [PLAIN]); } catch (e) { bad = e; }
71
+ ok(bad && bad.code === '23514', 'owner can never be granted by hand — it is the seat');
72
+ ok((await q(`SELECT needs FROM claimable_tasks WHERE id=$1`, [T_SERVER]))[0].needs.includes('server'),
73
+ 'the claimable_tasks view carries needs (core_285 rebuilt it)');
74
+
75
+ // The feed.
76
+ ok(JSON.stringify(ours(await db.listClaimableTasks({ builderId: PLAIN }))) === JSON.stringify([T_NONE]),
77
+ 'without server access or the seat: only the task that needs nothing');
78
+ ok(JSON.stringify(ours(await db.listClaimableTasks({ builderId: OWNER }))) === JSON.stringify([T_NONE, T_OWNER]),
79
+ 'the owner sees owner work in their own list');
80
+ ok(JSON.stringify(ours(await db.listClaimableTasks({ builderId: GRANTED }))) === JSON.stringify([T_NONE, T_SERVER]),
81
+ 'server access shows server work');
82
+ ok(JSON.stringify(ours(await db.listClaimableTasks({ builderId: OWNER, needs: 'owner' }))) === JSON.stringify([T_OWNER]),
83
+ '?needs=owner is the owner\'s waiting-on-you queue');
84
+ ok(ours(await db.listTasks({ versionId: VER })).length === 3, 'every task stays readable on the board');
85
+ ok(JSON.stringify(ours(await db.listTasks({ versionId: VER, needs: 'server' }))) === JSON.stringify([T_SERVER]),
86
+ 'the board filters by need');
87
+
88
+ // The claim gate, three surfaces.
89
+ let threw = null;
90
+ try { await db.claimTask({ taskId: String(T_SERVER), builderId: String(PLAIN), worktreeName: 'tn-plain' }); } catch (e) { threw = e; }
91
+ ok(threw && threw.code === 'NEEDS_SERVER_ACCESS' && /server access/.test(threw.message), 'claimTask refuses server work without the grant, in plain words');
92
+ const dry = await db.validateClaimBatch({ taskIds: [T_SERVER], builderId: String(PLAIN) });
93
+ ok(JSON.stringify(dry).includes('NEEDS_SERVER_ACCESS'), 'the dry run greys it out too');
94
+ let batchThrew = null;
95
+ try { await db.claimTasksBatch({ taskIds: [T_SERVER], builderId: String(PLAIN), worktreePrefix: 'tn-batch' }); } catch (e) { batchThrew = e; }
96
+ ok((batchThrew && batchThrew.failures || []).some((f) => Number(f.task_id) === T_SERVER && f.reason === 'NEEDS_SERVER_ACCESS'),
97
+ 'the batch path refuses it the same way');
98
+ const claim = await db.claimTask({ taskId: String(T_SERVER), builderId: String(GRANTED), worktreeName: 'tn-granted' });
99
+ ok(claim && Number(claim.task_id) === T_SERVER, 'with the grant, the claim succeeds');
100
+ const ownerClaim = await db.claimTask({ taskId: String(T_OWNER), builderId: String(OWNER), worktreeName: 'tn-owner' });
101
+ ok(ownerClaim && Number(ownerClaim.task_id) === T_OWNER, 'owner work is claimable by the owner');
102
+
103
+ console.log(`\ntask_needs_db (task 1004950): ${pass} passed, 0 failed`);
104
+ } catch (e) {
105
+ console.error('task_needs_db.mjs FAILED:', e.stack || e.message);
106
+ process.exitCode = 1;
107
+ } finally {
108
+ await cleanup();
109
+ await pool.end();
110
+ }