@bongos/core 1.23.22 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/.bongos-core.json +902 -407
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +90 -0
  27. package/clients/bongos-client/index.cjs +90 -0
  28. package/clients/bongos-client/index.d.ts +149 -4
  29. package/clients/bongos-client/index.mjs +90 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  33. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  34. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  35. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  36. package/docs/adr/README.md +5 -1
  37. package/docs/api/openapi.json +8555 -5876
  38. package/docs/api-reference.md +63 -10
  39. package/docs/architecture.md +37 -1
  40. package/docs/canonical-permissions.md +1 -1
  41. package/docs/copy-inventory.md +420 -337
  42. package/docs/copy-registry.json +1321 -500
  43. package/docs/file-map.md +1 -0
  44. package/docs/module-api-changelog.md +5 -0
  45. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  46. package/docs/onboarding/diagrams/README.md +1 -1
  47. package/docs/onboarding/drachmae-and-karma.md +15 -15
  48. package/docs/onboarding/primer.md +2 -2
  49. package/docs/page-inventory.json +55 -6
  50. package/docs/page-readings.json +1185 -1011
  51. package/docs/recipes/google-sign-in.md +60 -0
  52. package/docs/recipes/ops-gotchas.md +1 -1
  53. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  54. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  55. package/migrations/core_283_payout_approvals.sql +183 -0
  56. package/migrations/core_284_payout_gates.sql +179 -0
  57. package/migrations/core_285_task_needs.sql +74 -0
  58. package/modules/ai-studio/billing.js +67 -0
  59. package/modules/ai-studio/context.js +29 -0
  60. package/modules/ai-studio/db.js +172 -0
  61. package/modules/ai-studio/gemini.js +75 -0
  62. package/modules/ai-studio/generate.js +119 -0
  63. package/modules/ai-studio/keys.js +73 -0
  64. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  65. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  66. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  67. package/modules/ai-studio/openrouter.js +90 -0
  68. package/modules/ai-studio/payments.js +105 -0
  69. package/modules/ai-studio/pricing.js +92 -0
  70. package/modules/ai-studio/routes/ai-studio.js +235 -5
  71. package/modules/ai-studio/studio.js +121 -0
  72. package/modules/autonomy/fence.js +64 -1
  73. package/modules/builder-settings/builder-needs.js +27 -1
  74. package/modules/copy-desk/routes/copy-desk.js +5 -0
  75. package/modules/economy/achievements.js +22 -10
  76. package/modules/economy/cost-classify.js +9 -0
  77. package/modules/economy/cost-write-guard.js +52 -0
  78. package/modules/economy/cost.js +2 -1
  79. package/modules/economy/credits.js +18 -15
  80. package/modules/economy/goal-reward.js +161 -0
  81. package/modules/economy/reward.js +4 -0
  82. package/modules/economy/routes/cost.js +33 -7
  83. package/modules/government/catalog.js +6 -0
  84. package/modules/government/db.js +34 -0
  85. package/modules/government/docket.js +36 -0
  86. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  87. package/modules/government/pollers/board-expiry.js +108 -20
  88. package/modules/government/protected-surfaces.json +1 -0
  89. package/modules/government/routes/government.js +22 -0
  90. package/modules/government/routing.js +39 -5
  91. package/modules/government/waiting.js +74 -0
  92. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  93. package/modules/hall-ui/public/ai-studio.css +193 -0
  94. package/modules/hall-ui/public/ai-studio.html +98 -0
  95. package/modules/hall-ui/public/ai-studio.js +556 -0
  96. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  97. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  98. package/modules/hall-ui/public/blockers-lib.js +15 -1
  99. package/modules/hall-ui/public/blockers.html +8 -0
  100. package/modules/hall-ui/public/blockers.js +1 -1
  101. package/modules/hall-ui/public/claim-action.js +3 -0
  102. package/modules/hall-ui/public/collab.html +12 -0
  103. package/modules/hall-ui/public/craft-mode.js +4 -1
  104. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  105. package/modules/hall-ui/public/drachmae.html +1 -1
  106. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  107. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  108. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  109. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  110. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  111. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  115. package/modules/hall-ui/public/goal-inbox.js +1 -1
  116. package/modules/hall-ui/public/goals-render.js +4 -0
  117. package/modules/hall-ui/public/goals.html +8 -0
  118. package/modules/hall-ui/public/gov-living.js +6 -2
  119. package/modules/hall-ui/public/hall-ask.css +5 -0
  120. package/modules/hall-ui/public/hall-ask.js +39 -3
  121. package/modules/hall-ui/public/notes-panel.css +20 -0
  122. package/modules/hall-ui/public/notes-panel.js +146 -0
  123. package/modules/hall-ui/public/project-settings.html +1 -1
  124. package/modules/hall-ui/public/project-settings.js +7 -5
  125. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  126. package/modules/hall-ui/public/settings.html +15 -0
  127. package/modules/hall-ui/public/street-deck.js +12 -3
  128. package/modules/hall-ui/public/studio.css +90 -8
  129. package/modules/hall-ui/public/studio.html +39 -10
  130. package/modules/hall-ui/public/studio.js +148 -18
  131. package/modules/hall-ui/public/task.html +2 -0
  132. package/modules/hall-ui/public/task.js +4 -0
  133. package/modules/hall-ui/public/tweak-editor.js +12 -3
  134. package/modules/hall-ui/public/welcome-boot.css +42 -6
  135. package/modules/hall-ui/public/welcome-boot.js +20 -6
  136. package/modules/hall-ui/public/work.js +20 -0
  137. package/modules/hall-ui/records/ai-studio.md +36 -0
  138. package/modules/hall-ui/records/blockers-people.md +24 -0
  139. package/modules/lifecycle/db-claims.js +32 -4
  140. package/modules/lifecycle/db-goals.js +18 -5
  141. package/modules/lifecycle/db-tasks.js +15 -6
  142. package/modules/lifecycle/done-when.js +23 -1
  143. package/modules/lifecycle/lifecycle.js +5 -0
  144. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  145. package/modules/lifecycle/module.json +2 -1
  146. package/modules/lifecycle/notes.js +219 -0
  147. package/modules/lifecycle/page-notice.js +124 -0
  148. package/modules/lifecycle/page-tweak-artist.js +18 -1
  149. package/modules/lifecycle/publish-reconciler.js +67 -20
  150. package/modules/lifecycle/routes/claims.js +5 -2
  151. package/modules/lifecycle/routes/goals.js +3 -1
  152. package/modules/lifecycle/routes/notes.js +91 -0
  153. package/modules/lifecycle/routes/project-settings.js +43 -35
  154. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  155. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  156. package/modules/lifecycle/routes/tasks.js +12 -12
  157. package/modules/lifecycle/task-needs-gate.js +70 -0
  158. package/modules/market-watch/CLAUDE.md +15 -0
  159. package/modules/market-watch/db.js +340 -0
  160. package/modules/market-watch/key.js +58 -0
  161. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  162. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  163. package/modules/market-watch/module.json +26 -0
  164. package/modules/market-watch/pollers/hub-seed.js +54 -0
  165. package/modules/market-watch/pollers/weekly-run.js +100 -0
  166. package/modules/market-watch/research.js +350 -0
  167. package/modules/market-watch/run.js +245 -0
  168. package/modules/market-watch/seed/market-data.json +38 -0
  169. package/modules/market-watch/seed/partners-data.json +497 -0
  170. package/modules/market-watch/seed.js +252 -0
  171. package/modules/market-watch/store.js +230 -0
  172. package/modules/memory/routes/memory.js +14 -2
  173. package/modules/memory/routes/search.js +8 -2
  174. package/modules/platform-identity/account-logins.js +192 -0
  175. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  176. package/modules/platform-identity/platform-identity.js +5 -4
  177. package/modules/platform-identity/routes/my-projects.js +6 -0
  178. package/modules/platform-identity/routes/profile.js +18 -0
  179. package/modules/platform-identity/routes/public-profile.js +10 -1
  180. package/modules/platform-identity/routes/sso.js +26 -1
  181. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  182. package/modules/provisioning/credential-preflight.js +13 -3
  183. package/modules/provisioning/pollers/app-liveness.js +25 -5
  184. package/modules/provisioning/routes/provisioning.js +1 -1
  185. package/modules/provisioning/starter-bundles.js +6 -2
  186. package/modules/public-landing/public/account.html +10 -2
  187. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  188. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  189. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  190. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  191. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  192. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  193. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  194. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  195. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  196. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  197. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  198. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  199. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  200. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  201. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  202. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  203. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  204. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  205. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  206. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  207. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  208. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  209. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  210. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  211. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  212. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  213. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  214. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  215. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  216. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  217. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  218. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  219. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  220. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  221. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  222. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  223. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  224. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  225. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  226. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  227. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  228. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  229. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  230. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  231. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  232. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  233. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  234. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  235. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  236. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  237. package/modules/public-landing/public/community.html +1 -1
  238. package/modules/public-landing/public/connections.html +2 -2
  239. package/modules/public-landing/public/guild.html +1 -1
  240. package/modules/public-landing/public/invitations.html +2 -2
  241. package/modules/public-landing/public/projects.html +5 -5
  242. package/modules/raise/views/nda.html +2 -0
  243. package/modules/specialities/db.js +32 -1
  244. package/modules/specialities/routes/specialities.js +10 -6
  245. package/modules/status-ui/public/index.html +1 -1
  246. package/package-lock.json +2 -2
  247. package/package.json +1 -1
  248. package/release-notes.json +220 -0
  249. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  250. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  251. package/scripts/bongos/claim.js +22 -1
  252. package/scripts/bongos/db-backup-nightly.sh +29 -0
  253. package/scripts/bongos/deploy-divergence.js +7 -0
  254. package/scripts/bongos/doc-cli-guard.js +6 -0
  255. package/scripts/bongos/fitness.js +3 -2
  256. package/scripts/bongos/gen-diagrams.js +3 -3
  257. package/scripts/bongos/lib.sh +4 -5
  258. package/scripts/bongos/migration-namespace.js +13 -1
  259. package/scripts/bongos/plain-cards.js +4 -0
  260. package/scripts/bongos/provision-github-setup.js +3 -1
  261. package/scripts/bongos/provision-units.js +6 -1
  262. package/scripts/bongos/publish-manifest.js +3 -0
  263. package/scripts/bongos/release.js +2 -0
  264. package/scripts/bongos/run-unit-tests.js +268 -82
  265. package/scripts/bongos/seed-tasks.example.json +56 -0
  266. package/scripts/bongos/seed-tasks.js +222 -0
  267. package/scripts/bongos/seed-verify-criteria.js +1 -2
  268. package/scripts/bongos/ship-finish.js +2 -0
  269. package/scripts/bongos/ship-resume.js +2 -0
  270. package/scripts/bongos/strand-lease.js +250 -0
  271. package/scripts/bongos/strand-watch.js +26 -5
  272. package/scripts/bongos/version-literal-guard.js +2 -2
  273. package/scripts/discord/post.js +4 -3
  274. package/scripts/gds/seed-tasks.js +7 -0
  275. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  276. package/scripts/hall-preview/server.js +21 -0
  277. package/src/bongos/auth-config.js +29 -1
  278. package/src/bongos/auth-discord.js +3 -3
  279. package/src/bongos/auth-github.js +3 -6
  280. package/src/bongos/auth-google.js +217 -0
  281. package/src/bongos/device-approval.js +140 -0
  282. package/src/bongos/llm-pricing.js +17 -1
  283. package/src/bongos/metrics.js +48 -2
  284. package/src/bongos/middleware/rate-limit.js +30 -11
  285. package/src/bongos/module-scope-map.js +12 -1
  286. package/src/bongos/optimizer.js +15 -1
  287. package/src/bongos/payout-approvals.js +342 -0
  288. package/src/bongos/payout-gates.js +364 -0
  289. package/src/bongos/prelaunch-gate.js +7 -9
  290. package/src/bongos/prelaunch-switch.js +20 -0
  291. package/src/bongos/route-rank-check.js +3 -0
  292. package/src/bongos/routes/auth-google.js +246 -0
  293. package/src/bongos/routes/auth.js +38 -1
  294. package/src/bongos/routes/backup.js +33 -9
  295. package/src/bongos/routes/builders.js +57 -30
  296. package/src/bongos/routes/github.js +9 -0
  297. package/src/bongos/routes/me.js +18 -0
  298. package/src/bongos/routes/my-payouts.js +133 -0
  299. package/src/bongos/routes/payout-approvals.js +312 -0
  300. package/src/bongos/routes/payout-gates.js +179 -0
  301. package/src/bongos/routes.js +15 -1
  302. package/src/bongos/secret-box.js +18 -3
  303. package/src/bongos/serve-internal.js +47 -19
  304. package/src/bongos/sign-in-pages.js +138 -0
  305. package/src/bongos/task-needs.js +146 -0
  306. package/src/bongos-downloads.js +18 -1
  307. package/src/instance-config.js +28 -0
  308. package/src/module-api.js +14 -1
  309. package/tests/account_logins.mjs +326 -0
  310. package/tests/account_logins_db.mjs +161 -0
  311. package/tests/ai_studio_billing.mjs +253 -0
  312. package/tests/ai_studio_generate.mjs +282 -0
  313. package/tests/ai_studio_keys.mjs +159 -0
  314. package/tests/ai_studio_requests.mjs +319 -0
  315. package/tests/auth_backchannel_logout_route.mjs +201 -0
  316. package/tests/auto_satisfy_criteria.mjs +10 -1
  317. package/tests/autobongos_tweak_lane.mjs +144 -0
  318. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  319. package/tests/backfill_subagent_rewards.mjs +17 -1
  320. package/tests/backup_trigger_lock.mjs +119 -0
  321. package/tests/blockers_people.mjs +198 -0
  322. package/tests/bounded_rate_maps.mjs +184 -0
  323. package/tests/claimable_blocker_status.mjs +1 -1
  324. package/tests/claims_batch_routes.mjs +421 -0
  325. package/tests/config_read_cache.mjs +171 -0
  326. package/tests/copy_desk_page_editor.mjs +12 -0
  327. package/tests/cost_write_guard.mjs +200 -0
  328. package/tests/craft_achievements.mjs +33 -10
  329. package/tests/criterion_goal_attach.mjs +1 -1
  330. package/tests/db_backup_nightly.mjs +73 -1
  331. package/tests/deploy_divergence_line.mjs +7 -0
  332. package/tests/effective_visibility_predicate.mjs +12 -0
  333. package/tests/fitness.mjs +19 -0
  334. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  335. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  336. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  337. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  338. package/tests/github_one_trip.mjs +34 -0
  339. package/tests/github_repo_visibility.mjs +20 -0
  340. package/tests/goal_achievement.mjs +30 -1
  341. package/tests/goal_achievement_reward.mjs +205 -0
  342. package/tests/goal_sort_staleness.mjs +2 -1
  343. package/tests/goal_tier.mjs +11 -1
  344. package/tests/google_fonts_nonblocking.mjs +84 -0
  345. package/tests/google_sign_in.mjs +401 -0
  346. package/tests/government_board_expiry.mjs +94 -0
  347. package/tests/government_protected_surfaces.mjs +1 -1
  348. package/tests/government_routes.mjs +1 -1
  349. package/tests/hall_ai_studio.mjs +347 -0
  350. package/tests/hall_page_gate_map.mjs +3 -0
  351. package/tests/hall_studio_home.mjs +61 -2
  352. package/tests/home_attention_cards.mjs +1 -1
  353. package/tests/human_signal.mjs +141 -0
  354. package/tests/install_script_reader.mjs +51 -0
  355. package/tests/integration_fixture_ids.mjs +82 -0
  356. package/tests/invite_expiry_db.mjs +13 -1
  357. package/tests/lib_sh_resolution.mjs +0 -4
  358. package/tests/market_watch_data.mjs +244 -0
  359. package/tests/market_watch_data_db.mjs +308 -0
  360. package/tests/market_watch_module.mjs +114 -0
  361. package/tests/market_watch_run.mjs +503 -0
  362. package/tests/metrics.mjs +124 -0
  363. package/tests/migration_namespace.mjs +14 -0
  364. package/tests/module_api.mjs +2 -0
  365. package/tests/module_loader.mjs +1 -1
  366. package/tests/notes.mjs +182 -0
  367. package/tests/own_scope_guard.mjs +5 -0
  368. package/tests/page_tweak_marker_sql.mjs +76 -0
  369. package/tests/page_tweak_no_block.mjs +135 -0
  370. package/tests/page_tweak_notice.mjs +103 -0
  371. package/tests/payout_approvals.mjs +253 -0
  372. package/tests/payout_approvals_db.mjs +106 -0
  373. package/tests/payout_gates.mjs +322 -0
  374. package/tests/payout_gates_db.mjs +113 -0
  375. package/tests/planning_session_skill.mjs +13 -3
  376. package/tests/project_settings_page.mjs +29 -5
  377. package/tests/project_settings_spend_cap_route.mjs +235 -0
  378. package/tests/promotion_sweep.mjs +29 -1
  379. package/tests/provision.mjs +3 -2
  380. package/tests/provisioning_app_status.mjs +18 -0
  381. package/tests/provisioning_credential_preflight.mjs +53 -0
  382. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  383. package/tests/publish_reconciler_overlap.mjs +60 -0
  384. package/tests/rank_change_broadcast_port.mjs +151 -0
  385. package/tests/seed_tasks.mjs +176 -0
  386. package/tests/seed_verify_criteria.mjs +2 -6
  387. package/tests/site_classified_once.mjs +142 -0
  388. package/tests/speciality_db_shape.mjs +23 -0
  389. package/tests/speciality_session_skills.mjs +48 -2
  390. package/tests/strand_lease.mjs +249 -0
  391. package/tests/strand_watch.mjs +10 -0
  392. package/tests/task_criteria_routes.mjs +275 -0
  393. package/tests/task_criteria_unlink_db.mjs +123 -0
  394. package/tests/task_needs.mjs +233 -0
  395. package/tests/task_needs_db.mjs +110 -0
  396. package/tests/unit_runner_serial.mjs +266 -43
  397. package/tests/version_literals.mjs +5 -7
  398. package/tests/welcome_boot.mjs +16 -5
  399. package/tests/welcome_boot_browser.mjs +36 -4
  400. package/docs/recipes/bongos-cli-release.md +0 -66
  401. package/scripts/bongos/backfill-disciplines.js +0 -144
  402. package/scripts/bongos/backfill-session-earnings.js +0 -123
  403. package/scripts/bongos/backfill-session-types.js +0 -87
  404. package/scripts/bongos/build-bongos.js +0 -94
  405. package/scripts/bongos/cleanup-orphans.js +0 -74
  406. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  407. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  408. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  409. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  410. package/scripts/bongos/smoke-batch-release.sh +0 -316
  411. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  412. package/scripts/bongos/smoke-dependencies.sh +0 -143
  413. package/scripts/gds/backfill-disciplines.js +0 -7
  414. package/scripts/gds/backfill-session-earnings.js +0 -7
  415. package/scripts/gds/backfill-session-types.js +0 -7
  416. package/scripts/gds/cleanup-orphans.js +0 -7
  417. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  418. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  419. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  420. package/scripts/gds/smoke-batch-claim.sh +0 -8
  421. package/scripts/gds/smoke-batch-release.sh +0 -8
  422. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  423. package/scripts/gds/smoke-dependencies.sh +0 -8
  424. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -429,9 +429,26 @@ async function openBoardItem({ subjectType, subjectId, openedBy = null, constitu
429
429
  RETURNING *`,
430
430
  [subjectType, subjectId, openedBy, windowMinutes, JSON.stringify(constitution || {})],
431
431
  );
432
+ // Tell the expiry poller (task 1004960): it sleeps until the soonest deadline
433
+ // it knows of, so a window shorter than its idle wait must wake it sooner.
434
+ // closes_at is now() + the window, so the window IS the time until close.
435
+ // A listener the poller registers (not a require: the poller requires this
436
+ // file). Never let a hint fail an open.
437
+ if (rows[0] && windowMinutes != null) {
438
+ for (const fn of deadlineListeners) {
439
+ try { fn(Number(windowMinutes) * 60_000); } catch (_) { /* best-effort */ }
440
+ }
441
+ }
432
442
  return rows[0] || null;
433
443
  }
434
444
 
445
+ // Who wants to hear that a timed window opened (the expiry poller's start()).
446
+ const deadlineListeners = new Set();
447
+ function onBoardDeadline(fn) {
448
+ deadlineListeners.add(fn);
449
+ return () => deadlineListeners.delete(fn);
450
+ }
451
+
435
452
  async function getOpenBoardItem(subjectType, subjectId) {
436
453
  const { rows } = await pool.query(
437
454
  `SELECT * FROM government_board_items
@@ -1114,6 +1131,21 @@ async function getExpiredOpenBoardItems(limit = 100) {
1114
1131
  return rows;
1115
1132
  }
1116
1133
 
1134
+ // The expiry poller's alarm clock (task 1004960): how many milliseconds until
1135
+ // the soonest OPEN deadline, measured on the DATABASE clock (so a skewed
1136
+ // process clock can never make the poller wake early or late), or null when no
1137
+ // open item has a clock. ONE read, served by the same partial index as the
1138
+ // sweep — the poller sleeps until this deadline instead of asking every minute.
1139
+ async function msUntilSoonestOpenDeadline() {
1140
+ const { rows } = await pool.query(
1141
+ `SELECT EXTRACT(EPOCH FROM (MIN(closes_at) - now())) * 1000 AS ms
1142
+ FROM government_board_items
1143
+ WHERE closed_at IS NULL AND closes_at IS NOT NULL`,
1144
+ );
1145
+ const ms = rows[0] ? rows[0].ms : null;
1146
+ return ms == null ? null : Number(ms);
1147
+ }
1148
+
1117
1149
  // The summons read (task 1003271): every OPEN item this builder could still
1118
1150
  // vote on, with the two facts that decide it — who authored the subject, and
1119
1151
  // whether this builder has already cast. Membership is NOT decided here: it
@@ -1260,6 +1292,8 @@ module.exports = {
1260
1292
  withdrawBoardItem,
1261
1293
  getIdeaCompleteness,
1262
1294
  getExpiredOpenBoardItems,
1295
+ msUntilSoonestOpenDeadline,
1296
+ onBoardDeadline,
1263
1297
  getLatestReturnedItemWithObjection,
1264
1298
  getLatestPassedItem,
1265
1299
  listPassedSubjectIds,
@@ -85,6 +85,7 @@ const docketFloors = require('./docket-floors');
85
85
  const governmentConfig = require('./config');
86
86
  const asksDb = require('./asks-db');
87
87
  const routing = require('./routing');
88
+ const waiting = require('./waiting');
88
89
  const { ownerOf } = require('./leads');
89
90
 
90
91
  const log = api.logger('government');
@@ -847,6 +848,40 @@ async function loadSeats(government) {
847
848
  }
848
849
  }
849
850
 
851
+ // OPEN BLOCKERS UNDER THE PEOPLE THEY WAIT ON (task 1004999). The blockers source is
852
+ // read exactly as the docket reads it (the ideas module's contribution, under its own
853
+ // `anyBuilder` gate — the list is public), each one carries its stored ask, and the
854
+ // seats are read once, uncached (ADR 0016). waiting.js groups them with routing.js's
855
+ // rule, so this answers "whose is it" exactly as the City does. A board seat's holders
856
+ // are read only when some blocker names one. → { available, people, nobody, yours }
857
+ // `available: false` names why nothing could be grouped — never an empty list
858
+ // standing in for "nobody is waiting".
859
+ async function blockersByPerson(builder, { contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
860
+ const c = contributions.sources.get('blockers');
861
+ if (!c) return { available: false, reason: 'off', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
862
+ const permissions = await resolver.resolveBuilderPermissions(builder.id);
863
+ const held = new Set(permissions || []);
864
+ const ctx = Object.freeze({ builderId: String(builder.id), rank: builder.rank || null, has: (p) => held.has(p) });
865
+ if (!gateAllows(c.gate, ctx)) return { available: false, reason: 'not_yours', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
866
+ let matters;
867
+ try {
868
+ const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
869
+ matters = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source: 'blockers', building: c.building || 'planning' })).filter(Boolean);
870
+ } catch (err) {
871
+ log.warn({ err: err && err.message }, 'blockers by person: blockers unreadable');
872
+ return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
873
+ }
874
+ await attachAsks(matters, asksDb.asksFor, timeoutMs);
875
+ const boardSeat = matters.some((m) => /^board:/.test(routing.seatOf(m)));
876
+ const government = boardSeat
877
+ ? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
878
+ : { holders: {} };
879
+ const seats = await loadSeats(government);
880
+ if (!seats) return { available: false, reason: 'unavailable', people: [], nobody: [], yours: waiting.forViewer(null, builder.id) };
881
+ const grouped = waiting.groupByPerson(matters, seats);
882
+ return { available: true, ...grouped, yours: waiting.forViewer(grouped, builder.id) };
883
+ }
884
+
850
885
  // One matter with its stored ask and the seats, for passing it on to `toSeat`
851
886
  // (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
852
887
  // board's holders are read only when a board seat is involved.
@@ -927,5 +962,6 @@ module.exports = {
927
962
  findMatter,
928
963
  findMatterFor,
929
964
  loadSeats,
965
+ blockersByPerson,
930
966
  matterWithSeats,
931
967
  };
@@ -0,0 +1,29 @@
1
+ -- government_035_payout_approve.sql — grant the new `payout.approve` atom to
2
+ -- Archon (task 1004913, goal 1000092 N9; ADR 0385 D4).
3
+ --
4
+ -- WHAT IT IS. The project's authorised person for money: signs a standing payout
5
+ -- rule, narrows or revokes it, holds a run in its pause window, approves a single
6
+ -- payout, signs an equity grant or a contract, and reads the approval log
7
+ -- (core_283_payout_approvals). The routes also demand the person's own web
8
+ -- session, so holding the atom on a CLI token is not enough to sign.
9
+ --
10
+ -- WHY THIS FILE EXISTS. RANK_SEED is derived from the catalog's floors, and the
11
+ -- drift guard (tests/government_seed.mjs) compares it against the union of the
12
+ -- run-once seed plus every later grant migration, per rank. An archon-floor key
13
+ -- is held by archon only.
14
+ --
15
+ -- Additive only: one new permission key granted to one rank; nothing is revoked.
16
+ -- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
17
+ --
18
+ -- The `SELECT '<rank>', unnest(ARRAY[…])` shape is load-bearing: the drift guard
19
+ -- scans for it literally.
20
+
21
+ BEGIN;
22
+
23
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
24
+ SELECT 'archon', unnest(ARRAY[
25
+ 'payout.approve'
26
+ ])
27
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
28
+
29
+ COMMIT;
@@ -19,17 +19,32 @@
19
19
  // minute-cadence poller logging every tick would bury the journal (the
20
20
  // reconciler's own warn-once lesson).
21
21
  //
22
- // CADENCE: windows are minutes-granular (the recommended sitting is 90
23
- // minutes), so a one-minute sweep bounds lateness at ~1 minute — tight enough
24
- // that "closes on time" is honest, cheap enough that the query (one indexed
25
- // read; government_board_items_open_by_deadline exists for exactly this, per
26
- // government_003's own comment) is negligible. Concurrent windows are normal
22
+ // CADENCE (task 1004960): the poller SLEEPS UNTIL THE SOONEST DEADLINE instead
23
+ // of asking every minute. After each sweep it makes one MIN(closes_at) read
24
+ // (msUntilSoonestOpenDeadline, the same partial index) and arms its next run
25
+ // for just after that deadline — capped at MAX_WAIT_MS (5 minutes) so an item
26
+ // opened by some other process is still caught. A window opened in THIS process
27
+ // wakes it sooner through noteDeadline (db.openBoardItem calls it), so a
28
+ // one-minute window still closes on time. An idle instance with no open clock
29
+ // therefore reads twice per 5 minutes, not once a minute; a vote still closes
30
+ // within ~1 second of its deadline. While something is OVERDUE after a sweep
31
+ // (its close threw and rolled back) the retry waits RETRY_MS (the old one-minute
32
+ // cadence), never a hot loop. Runs never overlap: a tick that fires while the
33
+ // previous sweep is still in flight is skipped. Concurrent windows are normal
27
34
  // (owner decision 10): the sweep closes every due item, oldest deadline first.
28
35
 
29
36
  const board = require('../board');
30
37
  const db = require('../db');
31
38
 
32
- const INTERVAL_MS = Number(process.env.BOARD_EXPIRY_SWEEP_MS) || 60_000;
39
+ // The longest the poller ever sleeps (BOARD_EXPIRY_SWEEP_MS overrides it). It
40
+ // only bounds an item opened by ANOTHER process; this process's opens wake it.
41
+ const MAX_WAIT_MS = Number(process.env.BOARD_EXPIRY_SWEEP_MS) || 5 * 60_000;
42
+ // Kept as an alias: INTERVAL_MS was the fixed cadence before task 1004960.
43
+ const INTERVAL_MS = MAX_WAIT_MS;
44
+ // While an item is overdue after a sweep (its close failed and rolled back).
45
+ const RETRY_MS = 60_000;
46
+ // Wake just AFTER a deadline, since the sweep reads closes_at < now().
47
+ const GRACE_MS = 1_000;
33
48
  // A first sweep shortly after boot, so windows that expired while the process
34
49
  // was down (a deploy restart mid-sitting) close promptly rather than one full
35
50
  // interval late.
@@ -38,8 +53,21 @@ const FIRST_SWEEP_DELAY_MS = Number(process.env.BOARD_EXPIRY_FIRST_MS) || 15_000
38
53
  // handful of open windows (the board's subjects are rare by construction).
39
54
  const SWEEP_LIMIT = 100;
40
55
 
41
- let intervalTimer = null;
42
- let firstTimer = null;
56
+ let timer = null;
57
+ let dueAt = null; // when the armed timer fires (process clock, ms)
58
+ let started = false;
59
+ let running = false;
60
+ let hintAt = null; // soonest deadline (process clock) noted mid-sweep
61
+ let startDeps = {};
62
+ let unlisten = null;
63
+
64
+ // When to run next, given the ms until the soonest open deadline (null: none).
65
+ function nextDelayMs(msUntilSoonest) {
66
+ if (msUntilSoonest == null || !Number.isFinite(Number(msUntilSoonest))) return MAX_WAIT_MS;
67
+ const ms = Number(msUntilSoonest);
68
+ if (ms <= 0) return RETRY_MS; // still overdue after a sweep: retry, never spin
69
+ return Math.min(MAX_WAIT_MS, ms + GRACE_MS);
70
+ }
43
71
 
44
72
  // One sweep. Injectable deps keep it DB-free testable: listFn yields the due
45
73
  // items, closeFn closes one, log receives the disposition lines.
@@ -82,20 +110,80 @@ function start(deps = {}) {
82
110
  console.log('[board-expiry] disabled (BOARD_EXPIRY_DISABLED=1)');
83
111
  return;
84
112
  }
85
- if (intervalTimer) return; // already started
86
- const tick = () => {
87
- runSweep(deps).catch((e) => console.error(`[board-expiry] sweep crashed: ${e && e.message}`));
88
- };
89
- firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
90
- if (firstTimer.unref) firstTimer.unref();
91
- intervalTimer = setInterval(tick, INTERVAL_MS);
92
- if (intervalTimer.unref) intervalTimer.unref(); // never hold the process open on a restart
93
- console.log(`[board-expiry] started (every ${Math.round(INTERVAL_MS / 1000)}s; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
113
+ if (started) return; // already started
114
+ started = true;
115
+ startDeps = deps;
116
+ unlisten = db.onBoardDeadline(noteDeadline); // a window opened here wakes us sooner
117
+ arm(FIRST_SWEEP_DELAY_MS);
118
+ console.log(`[board-expiry] started (sleeps until the soonest deadline, at most ${Math.round(MAX_WAIT_MS / 1000)}s; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
119
+ }
120
+
121
+ function arm(delay) {
122
+ if (timer) clearTimeout(timer);
123
+ dueAt = Date.now() + delay;
124
+ timer = setTimeout(() => {
125
+ timer = null;
126
+ dueAt = null;
127
+ tick().catch((e) => console.error(`[board-expiry] sweep crashed: ${e && e.message}`));
128
+ }, delay);
129
+ if (timer.unref) timer.unref(); // never hold the process open on a restart
130
+ }
131
+
132
+ // One run: the sweep, then the one MIN(closes_at) read that sets the alarm.
133
+ // A run that starts while the previous one is still in flight is skipped —
134
+ // the running one re-arms when it finishes, so nothing is lost.
135
+ async function tick(deps = startDeps) {
136
+ if (running) return { skipped: true };
137
+ running = true;
138
+ let soonest = null;
139
+ try {
140
+ const out = await runSweep(deps);
141
+ try {
142
+ soonest = await (deps.soonestFn || db.msUntilSoonestOpenDeadline)();
143
+ } catch (e) {
144
+ (deps.error || console.error)(`[board-expiry] soonest-deadline read failed: ${e && e.message}`);
145
+ soonest = 0; // unknown: fall back to the one-minute retry, not the 5-minute cap
146
+ }
147
+ return out;
148
+ } finally {
149
+ running = false;
150
+ if (hintAt != null) {
151
+ const hint = hintAt - Date.now();
152
+ soonest = soonest == null ? hint : Math.min(soonest, hint);
153
+ hintAt = null;
154
+ }
155
+ if (started) arm(nextDelayMs(soonest));
156
+ }
157
+ }
158
+
159
+ // A window was opened that closes msUntilClose from now: wake sooner if the
160
+ // armed alarm is later than that deadline. Mid-sweep, remember it for the re-arm.
161
+ function noteDeadline(msUntilClose) {
162
+ if (!started || msUntilClose == null || !Number.isFinite(Number(msUntilClose))) return;
163
+ const ms = Math.max(0, Number(msUntilClose));
164
+ if (running) {
165
+ const at = Date.now() + ms;
166
+ hintAt = hintAt == null ? at : Math.min(hintAt, at);
167
+ return;
168
+ }
169
+ const delay = Math.min(MAX_WAIT_MS, ms + GRACE_MS);
170
+ if (dueAt == null || Date.now() + delay < dueAt) arm(delay);
94
171
  }
95
172
 
96
173
  function stop() {
97
- if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
98
- if (intervalTimer) { clearInterval(intervalTimer); intervalTimer = null; }
174
+ started = false;
175
+ hintAt = null;
176
+ if (unlisten) { unlisten(); unlisten = null; }
177
+ if (timer) { clearTimeout(timer); timer = null; }
178
+ dueAt = null;
179
+ }
180
+
181
+ // For tests: what is armed right now.
182
+ function _state() {
183
+ return { started, running, msUntilNextRun: dueAt == null ? null : dueAt - Date.now() };
99
184
  }
100
185
 
101
- module.exports = { start, stop, runSweep, INTERVAL_MS, SWEEP_LIMIT };
186
+ module.exports = {
187
+ start, stop, runSweep, tick, noteDeadline, nextDelayMs, _state,
188
+ INTERVAL_MS, MAX_WAIT_MS, RETRY_MS, GRACE_MS, SWEEP_LIMIT,
189
+ };
@@ -30,6 +30,7 @@
30
30
  { "glob": "src/bongos/auth-config.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
31
31
  { "glob": "src/bongos/auth-discord.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
32
32
  { "glob": "src/bongos/auth-github.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
33
+ { "glob": "src/bongos/auth-google.js", "group": "authz", "why": "task 1004993 (ADR 0396). The Google sign-in beside auth-github.js: it checks Google's ID token, runs the admission gate for a Google person and mints their web session, so it is the same trust surface as the GitHub flow." },
33
34
  { "glob": "src/bongos/auth-idp.js", "group": "authz", "why": "task 1004024. Carved out of auth.js (task 1003206): the OAuth flows, session-token minting, admission and IdP checks auth.js now delegates to. Registering auth.js alone protected the facade and none of them." },
34
35
  { "glob": "src/bongos/db.js", "group": "authz", "why": "rank tiers, REQUIRES_RANK_TIER — the CASE edited in the SEC #863 incident" },
35
36
  { "glob": "src/bongos/db-kernel.js", "group": "authz", "why": "task 1004024. The kernel db slice carved out of db.js (BV1.R69 / ADR 0091): builder identity and the rank reads the SEC #863 entry above exists for." },
@@ -767,6 +767,28 @@ module.exports = function buildGovernmentRouter() {
767
767
  // treasury; `rule` ∈ decide first consent majority unanimous signoff rule (or
768
768
  // null); `state` ∈ needs moving settled. Nothing is ever sample data: a source
769
769
  // with nothing recorded yet is `coming`, never a placeholder row.
770
+ // OPEN BLOCKERS UNDER PEOPLE (task 1004999): every open blocker grouped under each
771
+ // person its seat resolves to (routing.js, the City's own rule), and the viewer's own
772
+ // group. The docket's atom: it floors at xenos, so every builder can see whose a
773
+ // blocker is; resolving one keeps its own blocker.review gate.
774
+ router.get('/government/blockers/people', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
775
+ try {
776
+ res.json(await docket.blockersByPerson(req.builder));
777
+ } catch (err) {
778
+ log.error('[gds] GET /government/blockers/people', err);
779
+ res.fail('blockers_people_read_failed', { status: 500, message: 'internal error' });
780
+ }
781
+ });
782
+ router.get('/government/blockers/for-me', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
783
+ try {
784
+ const r = await docket.blockersByPerson(req.builder);
785
+ res.json({ available: r.available, reason: r.reason || null, ...r.yours });
786
+ } catch (err) {
787
+ log.error('[gds] GET /government/blockers/for-me', err);
788
+ res.fail('blockers_for_me_read_failed', { status: 500, message: 'internal error' });
789
+ }
790
+ });
791
+
770
792
  router.get('/government/docket', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
771
793
  try {
772
794
  res.json(await docket.docketFor(req.builder));
@@ -69,6 +69,35 @@ const idsOf = (list) => (Array.isArray(list) ? list : [])
69
69
  .filter((id) => id != null)
70
70
  .map(String);
71
71
 
72
+ // Who each builder id is, from the seats already read (task 1004999): the owner
73
+ // seat's Archons, the Leads in office and each building's holders. A route names
74
+ // people, not just a seat — "waits on Lars, the owner", not "waits on the owner".
75
+ // Pure; an id no seat row names reads as { id, login: null, name: null }.
76
+ function directoryOf(ctx) {
77
+ const c = ctx || {};
78
+ const dir = new Map();
79
+ const put = (id, login, name) => {
80
+ if (id == null) return;
81
+ const key = String(id);
82
+ const prev = dir.get(key);
83
+ if (prev && prev.name) return;
84
+ dir.set(key, { id: key, login: login || null, name: name || login || null });
85
+ };
86
+ for (const b of (c.owner && c.owner.builders) || []) put(b.id, b.login, b.name);
87
+ for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
88
+ if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
89
+ }
90
+ for (const list of Object.values(c.holders || {})) {
91
+ for (const p of Array.isArray(list) ? list : []) {
92
+ if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
93
+ }
94
+ }
95
+ return dir;
96
+ }
97
+ function peopleOf(ids, dir) {
98
+ return (ids || []).map((id) => dir.get(String(id)) || { id: String(id), login: null, name: null });
99
+ }
100
+
72
101
  // resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
73
102
  // asked the seat the matter names
74
103
  // seat the seat that answers it, after any fall
@@ -97,11 +126,13 @@ function resolveSeat(asked, ctx) {
97
126
  return toOwner();
98
127
  }
99
128
 
100
- // routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
101
- // { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
129
+ // routeMatter(matter, viewerId, ctx, now, dir) → the route the docket carries:
130
+ // { seat, label, asked, fell_to_owner, people, yours, nobody: null | { why, outcome } }
102
131
  // `yours` is the one answer to "is this on me": an open matter that needs a
103
132
  // person, on a seat the viewer sits in, that somebody can still answer.
104
- function routeMatter(matter, viewerId, ctx, now = Date.now()) {
133
+ // `dir` is directoryOf(ctx); routeAll builds it ONCE per request and passes it,
134
+ // so a docket of N matters does not rebuild the same name map N times.
135
+ function routeMatter(matter, viewerId, ctx, now = Date.now(), dir = directoryOf(ctx)) {
105
136
  const m = matter || {};
106
137
  const needs = m.state === 'needs';
107
138
  // a sitting's voters are the item's own (its snapshot), never a fall to the owner
@@ -120,6 +151,8 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
120
151
  label: r.label,
121
152
  asked: r.asked,
122
153
  fell_to_owner: r.fell_to_owner,
154
+ // who sits in that seat now, by name (task 1004999); [] for a sitting's own voters
155
+ people: peopleOf(r.builder_ids, dir),
123
156
  yours: needs && !nobody && !!viewer && sits,
124
157
  nobody,
125
158
  };
@@ -128,8 +161,9 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
128
161
  // Route every matter in place; → how many are the viewer's.
129
162
  function routeAll(matters, viewerId, ctx, now = Date.now()) {
130
163
  let yours = 0;
164
+ const dir = directoryOf(ctx);
131
165
  for (const m of matters || []) {
132
- m.route = routeMatter(m, viewerId, ctx, now);
166
+ m.route = routeMatter(m, viewerId, ctx, now, dir);
133
167
  if (m.route.yours) yours += 1;
134
168
  }
135
169
  return yours;
@@ -151,4 +185,4 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
151
185
  return null;
152
186
  }
153
187
 
154
- module.exports = { seatOf, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
188
+ module.exports = { seatOf, resolveSeat, directoryOf, peopleOf, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ // modules/government/waiting.js — open blockers grouped UNDER THE PEOPLE they wait on
4
+ // (task 1004999, goal 1000111).
5
+ //
6
+ // WHY. A blocker has no person on it: the blockers table has no assignee, and the only
7
+ // "who" is the seat its ask names (owner | lead:<b> | board:<b> | committee:…). So every
8
+ // surface listed every blocker to everyone, /blocker-review framed them all as the
9
+ // owner's, and nobody could open one place and see "these wait on me" — or see that a
10
+ // blocker was waiting on Lars rather than on "the owner".
11
+ //
12
+ // THE RULE. The seat is the truth; the person is resolved at READ time with the same
13
+ // routing the City uses (routing.js seatOf → resolveSeat), so the two can never
14
+ // disagree about whose a blocker is, and seating a new Lead moves their blockers with
15
+ // no blocker edited. Never copy a name onto a blocker.
16
+ //
17
+ // A blocker waits on EVERY person its seat resolves to (a board's members, or two
18
+ // Archons as the owner), so it is listed under each of them. One that resolves to
19
+ // nobody is listed under `nobody`, never dropped.
20
+ //
21
+ // PURE: no db. docket.js reads the blockers, their asks and the seats, then calls this.
22
+
23
+ const routing = require('./routing');
24
+
25
+ // One blocker as a line under a person: what it is, where it lives, and the seat it
26
+ // reaches that person through, in words ("as the owner", "as the Planning Lead").
27
+ function itemOf(m, r) {
28
+ return {
29
+ id: m.ref && m.ref.id != null ? String(m.ref.id) : String(m.id),
30
+ matter_id: String(m.id),
31
+ title: m.title,
32
+ href: m.href || null,
33
+ opened_at: m.opened_at || null,
34
+ where: m.where || null,
35
+ seat: r.seat,
36
+ as: r.label,
37
+ asked: r.asked,
38
+ fell_to_owner: !!r.fell_to_owner,
39
+ question: m.ask && m.ask.question ? String(m.ask.question) : null,
40
+ };
41
+ }
42
+
43
+ const byOpened = (a, b) => String(a.opened_at || '').localeCompare(String(b.opened_at || '')) || a.id.localeCompare(b.id, 'en', { numeric: true });
44
+
45
+ // groupByPerson(matters, seats) → { people: [{ person, count, items }], nobody: [items] }
46
+ // People with the most waiting first, then by name; inside a person, oldest first.
47
+ function groupByPerson(matters, seats) {
48
+ const dir = routing.directoryOf(seats);
49
+ const groups = new Map();
50
+ const nobody = [];
51
+ for (const m of matters || []) {
52
+ if (!m || m.state === 'settled') continue;
53
+ const r = routing.resolveSeat(routing.seatOf(m), seats);
54
+ const item = itemOf(m, r);
55
+ if (!r.builder_ids.length) { nobody.push(item); continue; }
56
+ for (const p of routing.peopleOf(r.builder_ids, dir)) {
57
+ if (!groups.has(p.id)) groups.set(p.id, { person: p, items: [] });
58
+ groups.get(p.id).items.push(item);
59
+ }
60
+ }
61
+ const people = [...groups.values()]
62
+ .map((g) => ({ person: g.person, count: g.items.length, items: g.items.sort(byOpened) }))
63
+ .sort((a, b) => b.count - a.count || String(a.person.name || a.person.id).localeCompare(String(b.person.name || b.person.id)));
64
+ return { people, nobody: nobody.sort(byOpened) };
65
+ }
66
+
67
+ // The viewer's own group: what waits on THEM. → { person, count, items } (count 0 when none).
68
+ function forViewer(grouped, viewerId) {
69
+ const id = viewerId == null ? null : String(viewerId);
70
+ const g = id && grouped && Array.isArray(grouped.people) ? grouped.people.find((x) => x.person.id === id) : null;
71
+ return g || { person: id ? { id, login: null, name: null } : null, count: 0, items: [] };
72
+ }
73
+
74
+ module.exports = { groupByPerson, forViewer };