@bongos/core 1.23.22 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.bongos-core.json +933 -413
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +96 -0
  27. package/clients/bongos-client/index.cjs +96 -0
  28. package/clients/bongos-client/index.d.ts +155 -4
  29. package/clients/bongos-client/index.mjs +96 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
  33. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  34. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  35. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  36. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  37. package/docs/adr/README.md +6 -2
  38. package/docs/api/openapi.json +8651 -5833
  39. package/docs/api-reference.md +66 -10
  40. package/docs/architecture.md +37 -1
  41. package/docs/canonical-permissions.md +1 -1
  42. package/docs/copy-inventory.md +427 -339
  43. package/docs/copy-registry.json +1376 -500
  44. package/docs/file-map.md +1 -0
  45. package/docs/module-api-changelog.md +7 -0
  46. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  47. package/docs/onboarding/diagrams/README.md +1 -1
  48. package/docs/onboarding/drachmae-and-karma.md +15 -15
  49. package/docs/onboarding/primer.md +2 -2
  50. package/docs/page-inventory.json +57 -7
  51. package/docs/page-readings.json +1210 -1027
  52. package/docs/recipes/google-sign-in.md +60 -0
  53. package/docs/recipes/ops-gotchas.md +1 -1
  54. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  55. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  56. package/migrations/core_283_payout_approvals.sql +183 -0
  57. package/migrations/core_284_payout_gates.sql +179 -0
  58. package/migrations/core_285_task_needs.sql +74 -0
  59. package/modules/ai-studio/billing.js +67 -0
  60. package/modules/ai-studio/context.js +29 -0
  61. package/modules/ai-studio/db.js +172 -0
  62. package/modules/ai-studio/gemini.js +75 -0
  63. package/modules/ai-studio/generate.js +119 -0
  64. package/modules/ai-studio/keys.js +73 -0
  65. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  66. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  67. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  68. package/modules/ai-studio/openrouter.js +90 -0
  69. package/modules/ai-studio/payments.js +105 -0
  70. package/modules/ai-studio/pricing.js +92 -0
  71. package/modules/ai-studio/routes/ai-studio.js +235 -5
  72. package/modules/ai-studio/studio.js +121 -0
  73. package/modules/autonomy/fence.js +64 -1
  74. package/modules/builder-settings/builder-needs.js +27 -1
  75. package/modules/copy-desk/routes/copy-desk.js +5 -0
  76. package/modules/economy/achievements.js +22 -10
  77. package/modules/economy/cost-classify.js +9 -0
  78. package/modules/economy/cost-write-guard.js +52 -0
  79. package/modules/economy/cost.js +2 -1
  80. package/modules/economy/credits.js +18 -15
  81. package/modules/economy/goal-reward.js +161 -0
  82. package/modules/economy/reward.js +4 -0
  83. package/modules/economy/routes/cost.js +33 -7
  84. package/modules/government/access-routes.js +70 -0
  85. package/modules/government/access.js +88 -0
  86. package/modules/government/asks.js +5 -2
  87. package/modules/government/catalog.js +6 -0
  88. package/modules/government/db.js +72 -0
  89. package/modules/government/docket.js +48 -2
  90. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  91. package/modules/government/migrations/government_036_access_holders.sql +33 -0
  92. package/modules/government/pollers/board-expiry.js +108 -20
  93. package/modules/government/protected-surfaces.json +1 -0
  94. package/modules/government/routes/government.js +31 -0
  95. package/modules/government/routing.js +49 -7
  96. package/modules/government/waiting.js +78 -0
  97. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  98. package/modules/hall-ui/public/ai-studio.css +193 -0
  99. package/modules/hall-ui/public/ai-studio.html +98 -0
  100. package/modules/hall-ui/public/ai-studio.js +556 -0
  101. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  102. package/modules/hall-ui/public/blockers-by-person.js +158 -0
  103. package/modules/hall-ui/public/blockers-lib.js +15 -1
  104. package/modules/hall-ui/public/blockers.html +8 -0
  105. package/modules/hall-ui/public/blockers.js +1 -1
  106. package/modules/hall-ui/public/claim-action.js +3 -0
  107. package/modules/hall-ui/public/collab.html +12 -0
  108. package/modules/hall-ui/public/craft-mode.js +4 -1
  109. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  110. package/modules/hall-ui/public/drachmae.html +1 -1
  111. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  115. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  120. package/modules/hall-ui/public/goal-inbox.js +1 -1
  121. package/modules/hall-ui/public/goals-render.js +4 -0
  122. package/modules/hall-ui/public/goals.html +8 -0
  123. package/modules/hall-ui/public/gov-living.js +6 -2
  124. package/modules/hall-ui/public/government-access.js +108 -0
  125. package/modules/hall-ui/public/government.html +21 -0
  126. package/modules/hall-ui/public/hall-ask.css +5 -0
  127. package/modules/hall-ui/public/hall-ask.js +39 -3
  128. package/modules/hall-ui/public/notes-panel.css +20 -0
  129. package/modules/hall-ui/public/notes-panel.js +146 -0
  130. package/modules/hall-ui/public/project-settings.html +1 -1
  131. package/modules/hall-ui/public/project-settings.js +7 -5
  132. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  133. package/modules/hall-ui/public/settings.html +15 -0
  134. package/modules/hall-ui/public/street-deck.js +12 -3
  135. package/modules/hall-ui/public/studio.css +90 -8
  136. package/modules/hall-ui/public/studio.html +39 -10
  137. package/modules/hall-ui/public/studio.js +148 -18
  138. package/modules/hall-ui/public/task.html +2 -0
  139. package/modules/hall-ui/public/task.js +4 -0
  140. package/modules/hall-ui/public/tweak-editor.js +12 -3
  141. package/modules/hall-ui/public/welcome-boot.css +42 -6
  142. package/modules/hall-ui/public/welcome-boot.js +20 -6
  143. package/modules/hall-ui/public/work.js +20 -0
  144. package/modules/hall-ui/records/ai-studio.md +36 -0
  145. package/modules/hall-ui/records/blockers-people.md +38 -0
  146. package/modules/lifecycle/db-claims.js +32 -4
  147. package/modules/lifecycle/db-goals.js +18 -5
  148. package/modules/lifecycle/db-tasks.js +15 -6
  149. package/modules/lifecycle/done-when.js +23 -1
  150. package/modules/lifecycle/lifecycle.js +5 -0
  151. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  152. package/modules/lifecycle/module.json +2 -1
  153. package/modules/lifecycle/notes.js +219 -0
  154. package/modules/lifecycle/page-notice.js +124 -0
  155. package/modules/lifecycle/page-tweak-artist.js +18 -1
  156. package/modules/lifecycle/publish-reconciler.js +67 -20
  157. package/modules/lifecycle/routes/claims.js +5 -2
  158. package/modules/lifecycle/routes/goals.js +3 -1
  159. package/modules/lifecycle/routes/notes.js +91 -0
  160. package/modules/lifecycle/routes/project-settings.js +43 -35
  161. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  162. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  163. package/modules/lifecycle/routes/tasks.js +12 -12
  164. package/modules/lifecycle/task-needs-gate.js +70 -0
  165. package/modules/market-watch/CLAUDE.md +15 -0
  166. package/modules/market-watch/db.js +340 -0
  167. package/modules/market-watch/key.js +58 -0
  168. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  169. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  170. package/modules/market-watch/module.json +26 -0
  171. package/modules/market-watch/pollers/hub-seed.js +54 -0
  172. package/modules/market-watch/pollers/weekly-run.js +100 -0
  173. package/modules/market-watch/research.js +350 -0
  174. package/modules/market-watch/run.js +245 -0
  175. package/modules/market-watch/seed/market-data.json +38 -0
  176. package/modules/market-watch/seed/partners-data.json +497 -0
  177. package/modules/market-watch/seed.js +252 -0
  178. package/modules/market-watch/store.js +230 -0
  179. package/modules/memory/routes/memory.js +14 -2
  180. package/modules/memory/routes/search.js +8 -2
  181. package/modules/platform-identity/account-logins.js +192 -0
  182. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  183. package/modules/platform-identity/platform-identity.js +5 -4
  184. package/modules/platform-identity/routes/my-projects.js +6 -0
  185. package/modules/platform-identity/routes/profile.js +18 -0
  186. package/modules/platform-identity/routes/public-profile.js +10 -1
  187. package/modules/platform-identity/routes/sso.js +26 -1
  188. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  189. package/modules/provisioning/credential-preflight.js +13 -3
  190. package/modules/provisioning/pollers/app-liveness.js +25 -5
  191. package/modules/provisioning/routes/provisioning.js +1 -1
  192. package/modules/provisioning/starter-bundles.js +6 -2
  193. package/modules/public-landing/public/account.html +10 -2
  194. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  195. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  196. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  197. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  198. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  199. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  200. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  201. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  202. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  203. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  205. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  209. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  210. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  213. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  214. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  221. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  222. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  223. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  224. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  225. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  226. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  227. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  228. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  229. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  230. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  233. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  244. package/modules/public-landing/public/community.html +1 -1
  245. package/modules/public-landing/public/connections.html +2 -2
  246. package/modules/public-landing/public/guild.html +1 -1
  247. package/modules/public-landing/public/invitations.html +2 -2
  248. package/modules/public-landing/public/projects.html +5 -5
  249. package/modules/raise/views/nda.html +2 -0
  250. package/modules/specialities/db.js +32 -1
  251. package/modules/specialities/routes/specialities.js +10 -6
  252. package/modules/status-ui/public/index.html +1 -1
  253. package/package-lock.json +2 -2
  254. package/package.json +1 -1
  255. package/release-notes.json +230 -0
  256. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  257. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  258. package/scripts/bongos/claim.js +22 -1
  259. package/scripts/bongos/db-backup-nightly.sh +29 -0
  260. package/scripts/bongos/deploy-divergence.js +7 -0
  261. package/scripts/bongos/doc-cli-guard.js +6 -0
  262. package/scripts/bongos/fitness.js +3 -2
  263. package/scripts/bongos/gen-diagrams.js +3 -3
  264. package/scripts/bongos/government-walk.js +3 -0
  265. package/scripts/bongos/lib.sh +4 -5
  266. package/scripts/bongos/migration-namespace.js +13 -1
  267. package/scripts/bongos/plain-cards.js +4 -0
  268. package/scripts/bongos/provision-github-setup.js +3 -1
  269. package/scripts/bongos/provision-units.js +6 -1
  270. package/scripts/bongos/publish-manifest.js +3 -0
  271. package/scripts/bongos/release.js +2 -0
  272. package/scripts/bongos/run-unit-tests.js +268 -82
  273. package/scripts/bongos/seed-tasks.example.json +56 -0
  274. package/scripts/bongos/seed-tasks.js +222 -0
  275. package/scripts/bongos/seed-verify-criteria.js +1 -2
  276. package/scripts/bongos/ship-finish.js +2 -0
  277. package/scripts/bongos/ship-resume.js +2 -0
  278. package/scripts/bongos/strand-lease.js +250 -0
  279. package/scripts/bongos/strand-watch.js +26 -5
  280. package/scripts/bongos/version-literal-guard.js +2 -2
  281. package/scripts/discord/post.js +4 -3
  282. package/scripts/gds/seed-tasks.js +7 -0
  283. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  284. package/scripts/hall-preview/server.js +21 -0
  285. package/src/bongos/auth-config.js +29 -1
  286. package/src/bongos/auth-discord.js +3 -3
  287. package/src/bongos/auth-github.js +3 -6
  288. package/src/bongos/auth-google.js +217 -0
  289. package/src/bongos/device-approval.js +140 -0
  290. package/src/bongos/llm-pricing.js +17 -1
  291. package/src/bongos/metrics.js +48 -2
  292. package/src/bongos/middleware/rate-limit.js +30 -11
  293. package/src/bongos/module-scope-map.js +12 -1
  294. package/src/bongos/optimizer.js +15 -1
  295. package/src/bongos/payout-approvals.js +342 -0
  296. package/src/bongos/payout-gates.js +364 -0
  297. package/src/bongos/prelaunch-gate.js +7 -9
  298. package/src/bongos/prelaunch-switch.js +20 -0
  299. package/src/bongos/route-rank-check.js +7 -0
  300. package/src/bongos/routes/auth-google.js +246 -0
  301. package/src/bongos/routes/auth.js +38 -1
  302. package/src/bongos/routes/backup.js +33 -9
  303. package/src/bongos/routes/builders.js +57 -30
  304. package/src/bongos/routes/github.js +9 -0
  305. package/src/bongos/routes/me.js +18 -0
  306. package/src/bongos/routes/my-payouts.js +133 -0
  307. package/src/bongos/routes/payout-approvals.js +312 -0
  308. package/src/bongos/routes/payout-gates.js +179 -0
  309. package/src/bongos/routes.js +15 -1
  310. package/src/bongos/secret-box.js +18 -3
  311. package/src/bongos/serve-internal.js +47 -19
  312. package/src/bongos/sign-in-pages.js +138 -0
  313. package/src/bongos/task-needs.js +146 -0
  314. package/src/bongos-downloads.js +18 -1
  315. package/src/instance-config.js +28 -0
  316. package/src/module-api.js +14 -1
  317. package/tests/access_holders.mjs +214 -0
  318. package/tests/account_logins.mjs +326 -0
  319. package/tests/account_logins_db.mjs +161 -0
  320. package/tests/ai_studio_billing.mjs +253 -0
  321. package/tests/ai_studio_generate.mjs +282 -0
  322. package/tests/ai_studio_keys.mjs +159 -0
  323. package/tests/ai_studio_requests.mjs +319 -0
  324. package/tests/auth_backchannel_logout_route.mjs +201 -0
  325. package/tests/auto_satisfy_criteria.mjs +10 -1
  326. package/tests/autobongos_tweak_lane.mjs +144 -0
  327. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  328. package/tests/backfill_subagent_rewards.mjs +17 -1
  329. package/tests/backup_trigger_lock.mjs +119 -0
  330. package/tests/blockers_people.mjs +198 -0
  331. package/tests/bounded_rate_maps.mjs +184 -0
  332. package/tests/claimable_blocker_status.mjs +1 -1
  333. package/tests/claims_batch_routes.mjs +421 -0
  334. package/tests/config_read_cache.mjs +171 -0
  335. package/tests/copy_desk_page_editor.mjs +12 -0
  336. package/tests/cost_write_guard.mjs +200 -0
  337. package/tests/craft_achievements.mjs +33 -10
  338. package/tests/criterion_goal_attach.mjs +1 -1
  339. package/tests/db_backup_nightly.mjs +73 -1
  340. package/tests/deploy_divergence_line.mjs +7 -0
  341. package/tests/effective_visibility_predicate.mjs +12 -0
  342. package/tests/fitness.mjs +19 -0
  343. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  344. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  345. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  346. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  347. package/tests/github_one_trip.mjs +34 -0
  348. package/tests/github_repo_visibility.mjs +20 -0
  349. package/tests/goal_achievement.mjs +30 -1
  350. package/tests/goal_achievement_reward.mjs +205 -0
  351. package/tests/goal_sort_staleness.mjs +2 -1
  352. package/tests/goal_tier.mjs +11 -1
  353. package/tests/google_fonts_nonblocking.mjs +84 -0
  354. package/tests/google_sign_in.mjs +401 -0
  355. package/tests/government_board_expiry.mjs +94 -0
  356. package/tests/government_board_membership.mjs +4 -1
  357. package/tests/government_protected_surfaces.mjs +1 -1
  358. package/tests/government_routes.mjs +1 -1
  359. package/tests/government_seed.mjs +1 -0
  360. package/tests/hall_ai_studio.mjs +347 -0
  361. package/tests/hall_page_gate_map.mjs +3 -0
  362. package/tests/hall_studio_home.mjs +61 -2
  363. package/tests/home_attention_cards.mjs +1 -1
  364. package/tests/human_signal.mjs +141 -0
  365. package/tests/install_script_reader.mjs +51 -0
  366. package/tests/integration_fixture_ids.mjs +82 -0
  367. package/tests/invite_expiry_db.mjs +13 -1
  368. package/tests/lib_sh_resolution.mjs +0 -4
  369. package/tests/market_watch_data.mjs +244 -0
  370. package/tests/market_watch_data_db.mjs +308 -0
  371. package/tests/market_watch_module.mjs +114 -0
  372. package/tests/market_watch_run.mjs +503 -0
  373. package/tests/metrics.mjs +124 -0
  374. package/tests/migration_namespace.mjs +14 -0
  375. package/tests/module_api.mjs +2 -0
  376. package/tests/module_loader.mjs +1 -1
  377. package/tests/notes.mjs +182 -0
  378. package/tests/own_scope_guard.mjs +5 -0
  379. package/tests/page_tweak_marker_sql.mjs +76 -0
  380. package/tests/page_tweak_no_block.mjs +135 -0
  381. package/tests/page_tweak_notice.mjs +103 -0
  382. package/tests/payout_approvals.mjs +253 -0
  383. package/tests/payout_approvals_db.mjs +106 -0
  384. package/tests/payout_gates.mjs +322 -0
  385. package/tests/payout_gates_db.mjs +113 -0
  386. package/tests/planning_session_skill.mjs +13 -3
  387. package/tests/project_settings_page.mjs +29 -5
  388. package/tests/project_settings_spend_cap_route.mjs +235 -0
  389. package/tests/promotion_sweep.mjs +29 -1
  390. package/tests/provision.mjs +3 -2
  391. package/tests/provisioning_app_status.mjs +18 -0
  392. package/tests/provisioning_credential_preflight.mjs +53 -0
  393. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  394. package/tests/publish_reconciler_overlap.mjs +60 -0
  395. package/tests/rank_change_broadcast_port.mjs +151 -0
  396. package/tests/seed_tasks.mjs +176 -0
  397. package/tests/seed_verify_criteria.mjs +2 -6
  398. package/tests/site_classified_once.mjs +142 -0
  399. package/tests/speciality_db_shape.mjs +23 -0
  400. package/tests/speciality_session_skills.mjs +48 -2
  401. package/tests/strand_lease.mjs +249 -0
  402. package/tests/strand_watch.mjs +10 -0
  403. package/tests/task_criteria_routes.mjs +275 -0
  404. package/tests/task_criteria_unlink_db.mjs +123 -0
  405. package/tests/task_needs.mjs +233 -0
  406. package/tests/task_needs_db.mjs +110 -0
  407. package/tests/unit_runner_serial.mjs +266 -43
  408. package/tests/version_literals.mjs +5 -7
  409. package/tests/welcome_boot.mjs +16 -5
  410. package/tests/welcome_boot_browser.mjs +36 -4
  411. package/docs/recipes/bongos-cli-release.md +0 -66
  412. package/scripts/bongos/backfill-disciplines.js +0 -144
  413. package/scripts/bongos/backfill-session-earnings.js +0 -123
  414. package/scripts/bongos/backfill-session-types.js +0 -87
  415. package/scripts/bongos/build-bongos.js +0 -94
  416. package/scripts/bongos/cleanup-orphans.js +0 -74
  417. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  418. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  419. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  420. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  421. package/scripts/bongos/smoke-batch-release.sh +0 -316
  422. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  423. package/scripts/bongos/smoke-dependencies.sh +0 -143
  424. package/scripts/gds/backfill-disciplines.js +0 -7
  425. package/scripts/gds/backfill-session-earnings.js +0 -7
  426. package/scripts/gds/backfill-session-types.js +0 -7
  427. package/scripts/gds/cleanup-orphans.js +0 -7
  428. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  429. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  430. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  431. package/scripts/gds/smoke-batch-claim.sh +0 -8
  432. package/scripts/gds/smoke-batch-release.sh +0 -8
  433. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  434. package/scripts/gds/smoke-dependencies.sh +0 -8
  435. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -34,6 +34,7 @@ const counts = require('../counts');
34
34
  const asks = require('../asks');
35
35
  const asksDb = require('../asks-db');
36
36
  const routing = require('../routing');
37
+ const { accessHandlers } = require('../access-routes');
37
38
  const governmentConfig = require('../config');
38
39
  const membershipPredicate = require('../membership-predicate');
39
40
  const positions = require('../positions');
@@ -689,6 +690,14 @@ module.exports = function buildGovernmentRouter() {
689
690
  }
690
691
  });
691
692
 
693
+ // ACCESS HOLDERS (task 1005000): who holds the keys outside Bongos, so an ask can wait
694
+ // on access:<key>. Read on the docket's atom; recorded by the owner. A record of who to
695
+ // ask, never authority (access.js). Bodies: access-routes.js.
696
+ const accessRoutes = accessHandlers({ db, log, parseId, validateOrRespond });
697
+ router.get('/government/access', api.requireBuilder, api.requirePermission('board.vote.cast'), accessRoutes.read);
698
+ router.post('/government/access/:accessKey/holders', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.grant);
699
+ router.delete('/government/access/:accessKey/holders/:builderId', api.requireBuilder, api.requirePermission('government.manage'), accessRoutes.revoke);
700
+
692
701
  // rank: perm board.vote.cast — the Board Room view: open items with their
693
702
  // clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). Gated on
694
703
  // the XENOS-floored vote atom, deliberately: seeing the ballot is part of
@@ -767,6 +776,28 @@ module.exports = function buildGovernmentRouter() {
767
776
  // treasury; `rule` ∈ decide first consent majority unanimous signoff rule (or
768
777
  // null); `state` ∈ needs moving settled. Nothing is ever sample data: a source
769
778
  // with nothing recorded yet is `coming`, never a placeholder row.
779
+ // OPEN BLOCKERS UNDER PEOPLE (task 1004999): every open blocker grouped under each
780
+ // person its seat resolves to (routing.js, the City's own rule), and the viewer's own
781
+ // group. The docket's atom: it floors at xenos, so every builder can see whose a
782
+ // blocker is; resolving one keeps its own blocker.review gate.
783
+ router.get('/government/blockers/people', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
784
+ try {
785
+ res.json(await docket.blockersByPerson(req.builder));
786
+ } catch (err) {
787
+ log.error('[gds] GET /government/blockers/people', err);
788
+ res.fail('blockers_people_read_failed', { status: 500, message: 'internal error' });
789
+ }
790
+ });
791
+ router.get('/government/blockers/for-me', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
792
+ try {
793
+ const r = await docket.blockersByPerson(req.builder);
794
+ res.json({ available: r.available, reason: r.reason || null, ...r.yours });
795
+ } catch (err) {
796
+ log.error('[gds] GET /government/blockers/for-me', err);
797
+ res.fail('blockers_for_me_read_failed', { status: 500, message: 'internal error' });
798
+ }
799
+ });
800
+
770
801
  router.get('/government/docket', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
771
802
  try {
772
803
  res.json(await docket.docketFor(req.builder));
@@ -38,6 +38,7 @@
38
38
  // request, uncached (ADR 0016), and routes every matter with routeAll.
39
39
 
40
40
  const { answerFor } = require('./leads');
41
+ const access = require('./access');
41
42
  const asks = require('./asks');
42
43
 
43
44
  const BUILDING_NAMES = Object.freeze({
@@ -59,6 +60,7 @@ function seatOf(matter) {
59
60
  function seatLabel(seat) {
60
61
  const s = String(seat || '');
61
62
  if (s === 'owner') return 'the owner';
63
+ if (/^access:/.test(s)) return access.accessLabel(s.slice(7));
62
64
  const m = /^(lead|board):([a-z]+)$/.exec(s);
63
65
  if (m) return `the ${BUILDING_NAMES[m[2]] || m[2]} ${m[1] === 'lead' ? 'Lead' : 'board'}`;
64
66
  return asks.seatWords(s);
@@ -69,12 +71,42 @@ const idsOf = (list) => (Array.isArray(list) ? list : [])
69
71
  .filter((id) => id != null)
70
72
  .map(String);
71
73
 
74
+ // Who each builder id is, from the seats already read (task 1004999): the owner
75
+ // seat's Archons, the Leads in office and each building's holders. A route names
76
+ // people, not just a seat — "waits on Lars, the owner", not "waits on the owner".
77
+ // Pure; an id no seat row names reads as { id, login: null, name: null }.
78
+ function directoryOf(ctx) {
79
+ const c = ctx || {};
80
+ const dir = new Map();
81
+ const put = (id, login, name) => {
82
+ if (id == null) return;
83
+ const key = String(id);
84
+ const prev = dir.get(key);
85
+ if (prev && prev.name) return;
86
+ dir.set(key, { id: key, login: login || null, name: name || login || null });
87
+ };
88
+ for (const b of (c.owner && c.owner.builders) || []) put(b.id, b.login, b.name);
89
+ for (const l of Array.isArray(c.leads) ? c.leads : Object.values(c.leads || {})) {
90
+ if (l && !l.removed_at) put(l.builder_id, l.github_login, l.display_name);
91
+ }
92
+ for (const list of [...Object.values(c.holders || {}), ...Object.values(c.access || {})]) {
93
+ for (const p of Array.isArray(list) ? list : []) {
94
+ if (p && typeof p === 'object') put(p.builder_id ?? p.id, p.github_login ?? p.login, p.display_name ?? p.name);
95
+ }
96
+ }
97
+ return dir;
98
+ }
99
+ function peopleOf(ids, dir) {
100
+ return (ids || []).map((id) => dir.get(String(id)) || { id: String(id), login: null, name: null });
101
+ }
102
+
72
103
  // resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
73
104
  // asked the seat the matter names
74
105
  // seat the seat that answers it, after any fall
75
106
  // builder_ids who sits there now ([] when nobody does)
76
107
  // ctx { leads: Lead rows in office, owner: leads ownerOf(archons) shape
77
- // { builders: [{ id }] }, holders: { building: [people] | null } }
108
+ // { builders: [{ id }] }, holders: { building: [people] | null },
109
+ // access: { key: [holder rows] } (task 1005000) }
78
110
  function resolveSeat(asked, ctx) {
79
111
  const c = ctx || {};
80
112
  const owner = c.owner || { builders: [] };
@@ -91,17 +123,24 @@ function resolveSeat(asked, ctx) {
91
123
  const held = idsOf((c.holders || {})[m[1]]);
92
124
  return held.length ? seated(asked, held) : toOwner();
93
125
  }
126
+ // task 1005000: an outside access — its recorded holders; nobody recorded → the owner.
127
+ if ((m = /^access:([a-z-]+)$/.exec(String(asked || ''))) && access.isAccessKey(m[1])) {
128
+ const held = idsOf((c.access || {})[m[1]]);
129
+ return held.length ? seated(asked, held) : toOwner();
130
+ }
94
131
  if ((m = /^committee:([a-z]+)\.[a-z0-9_]+$/.exec(String(asked || '')))) {
95
132
  return { ...resolveSeat(`lead:${m[1]}`, c), asked };
96
133
  }
97
134
  return toOwner();
98
135
  }
99
136
 
100
- // routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
101
- // { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
137
+ // routeMatter(matter, viewerId, ctx, now, dir) → the route the docket carries:
138
+ // { seat, label, asked, fell_to_owner, people, yours, nobody: null | { why, outcome } }
102
139
  // `yours` is the one answer to "is this on me": an open matter that needs a
103
140
  // person, on a seat the viewer sits in, that somebody can still answer.
104
- function routeMatter(matter, viewerId, ctx, now = Date.now()) {
141
+ // `dir` is directoryOf(ctx); routeAll builds it ONCE per request and passes it,
142
+ // so a docket of N matters does not rebuild the same name map N times.
143
+ function routeMatter(matter, viewerId, ctx, now = Date.now(), dir = directoryOf(ctx)) {
105
144
  const m = matter || {};
106
145
  const needs = m.state === 'needs';
107
146
  // a sitting's voters are the item's own (its snapshot), never a fall to the owner
@@ -120,6 +159,8 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
120
159
  label: r.label,
121
160
  asked: r.asked,
122
161
  fell_to_owner: r.fell_to_owner,
162
+ // who sits in that seat now, by name (task 1004999); [] for a sitting's own voters
163
+ people: peopleOf(r.builder_ids, dir),
123
164
  yours: needs && !nobody && !!viewer && sits,
124
165
  nobody,
125
166
  };
@@ -128,8 +169,9 @@ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
128
169
  // Route every matter in place; → how many are the viewer's.
129
170
  function routeAll(matters, viewerId, ctx, now = Date.now()) {
130
171
  let yours = 0;
172
+ const dir = directoryOf(ctx);
131
173
  for (const m of matters || []) {
132
- m.route = routeMatter(m, viewerId, ctx, now);
174
+ m.route = routeMatter(m, viewerId, ctx, now, dir);
133
175
  if (m.route.yours) yours += 1;
134
176
  }
135
177
  return yours;
@@ -144,11 +186,11 @@ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
144
186
  if (!m.ask) return { code: 'no_ask', status: 409, message: 'This matter has no ask to pass on yet.' };
145
187
  if (m.source === 'board') return { code: 'vote_not_passable', status: 409, message: 'A vote is its voters\' to answer; it cannot be passed on.' };
146
188
  const to = String(toSeat || '').trim();
147
- if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name> or board:<building>.' };
189
+ if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name>, board:<building> or access:<key>.' };
148
190
  const route = routeMatter(m, viewerId, ctx, now);
149
191
  if (!route.yours) return { code: 'not_yours', status: 403, message: 'Only whoever this waits on can pass it on.' };
150
192
  if (to === m.ask.waits_on || resolveSeat(to, ctx).seat === route.seat) return { code: 'same_seat', status: 409, message: 'It already waits on that seat.' };
151
193
  return null;
152
194
  }
153
195
 
154
- module.exports = { seatOf, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
196
+ module.exports = { seatOf, resolveSeat, directoryOf, peopleOf, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
@@ -0,0 +1,78 @@
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
+ const access = require('./access');
25
+
26
+ // One blocker as a line under a person: what it is, where it lives, and the seat it
27
+ // reaches that person through, in words ("as the owner", "as the Planning Lead").
28
+ function itemOf(m, r) {
29
+ return {
30
+ id: m.ref && m.ref.id != null ? String(m.ref.id) : String(m.id),
31
+ matter_id: String(m.id),
32
+ title: m.title,
33
+ href: m.href || null,
34
+ opened_at: m.opened_at || null,
35
+ where: m.where || null,
36
+ seat: r.seat,
37
+ as: r.label,
38
+ asked: r.asked,
39
+ fell_to_owner: !!r.fell_to_owner,
40
+ // task 1005000: a blocker only an outside-access holder can clear reads as its own
41
+ // category ("needs repo-owner access"), not as one more of the owner's.
42
+ needs: /^access:/.test(String(r.asked || '')) ? access.needsWords(String(r.asked).slice(7)) : null,
43
+ question: m.ask && m.ask.question ? String(m.ask.question) : null,
44
+ };
45
+ }
46
+
47
+ const byOpened = (a, b) => String(a.opened_at || '').localeCompare(String(b.opened_at || '')) || a.id.localeCompare(b.id, 'en', { numeric: true });
48
+
49
+ // groupByPerson(matters, seats) → { people: [{ person, count, items }], nobody: [items] }
50
+ // People with the most waiting first, then by name; inside a person, oldest first.
51
+ function groupByPerson(matters, seats) {
52
+ const dir = routing.directoryOf(seats);
53
+ const groups = new Map();
54
+ const nobody = [];
55
+ for (const m of matters || []) {
56
+ if (!m || m.state === 'settled') continue;
57
+ const r = routing.resolveSeat(routing.seatOf(m), seats);
58
+ const item = itemOf(m, r);
59
+ if (!r.builder_ids.length) { nobody.push(item); continue; }
60
+ for (const p of routing.peopleOf(r.builder_ids, dir)) {
61
+ if (!groups.has(p.id)) groups.set(p.id, { person: p, items: [] });
62
+ groups.get(p.id).items.push(item);
63
+ }
64
+ }
65
+ const people = [...groups.values()]
66
+ .map((g) => ({ person: g.person, count: g.items.length, items: g.items.sort(byOpened) }))
67
+ .sort((a, b) => b.count - a.count || String(a.person.name || a.person.id).localeCompare(String(b.person.name || b.person.id)));
68
+ return { people, nobody: nobody.sort(byOpened) };
69
+ }
70
+
71
+ // The viewer's own group: what waits on THEM. → { person, count, items } (count 0 when none).
72
+ function forViewer(grouped, viewerId) {
73
+ const id = viewerId == null ? null : String(viewerId);
74
+ const g = id && grouped && Array.isArray(grouped.people) ? grouped.people.find((x) => x.person.id === id) : null;
75
+ return g || { person: id ? { id, login: null, name: null } : null, count: 0, items: [] };
76
+ }
77
+
78
+ module.exports = { groupByPerson, forViewer };
@@ -0,0 +1,268 @@
1
+ // modules/hall-ui/public/ai-studio-lib.js — the pure rules of the Studio's AI
2
+ // pages, /builders/ai-studio (task 1004921, goal 1000136, ADR 0392).
3
+ //
4
+ // Built 1:1 on the owner's pick, option C of the AI Studio options canvas
5
+ // ("Three steps: pick, describe, make"), with the owner's addition that the
6
+ // results page lets the request be changed and made again. Everything here is
7
+ // pure: money in words, the receipt, the month's summary of the ledger, the
8
+ // waiting checklist, which view a URL names, and the reply's safe markup.
9
+ // ai-studio.js does the reads, the writes and the painting.
10
+ //
11
+ // Money is integer micro-dollars everywhere (1,000,000 = $1), as the server
12
+ // sends it. A PRICE with sub-cent digits shows four decimals ($0.0421); a
13
+ // whole-cent amount and a BALANCE show two ($12.40). A balance rounds DOWN, so
14
+ // the page never promises a cent that is not there.
15
+ //
16
+ // The file limits below are the upload checks' (modules/ai-studio/studio.js),
17
+ // repeated so the page can say no before a 4 MB upload starts. The server
18
+ // still checks every file; these only spare the wait.
19
+ //
20
+ // Node-require-safe: assignment to window is the only side effect, and only
21
+ // when a window exists (tests/hall_ai_studio.mjs requires it directly).
22
+
23
+ (function (root) {
24
+ 'use strict';
25
+
26
+ const MICRO_PER_USD = 1000000;
27
+ const MICRO_PER_CENT = 10000;
28
+ const MAX_FILES = 6;
29
+ const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
30
+ const MAX_PDF_BYTES = 8 * 1024 * 1024;
31
+ const IMAGE_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
32
+ const PDF_TYPE = 'application/pdf';
33
+ const FINISHED = Object.freeze(['done', 'failed', 'refused']);
34
+ const STEPS = Object.freeze(['pick', 'describe', 'make']);
35
+ const TOPUP_CHOICES = Object.freeze([5, 10, 20, 50]);
36
+ // The waiting checklist is paced by the clock: the server reports a request
37
+ // as queued or running, not which call it is on, so "wrote the answer" is
38
+ // ticked once a run has gone on long enough for the reply to be back.
39
+ const ANSWER_AFTER_SEC = 25;
40
+
41
+ function isMicro(v) { return Number.isSafeInteger(v); }
42
+
43
+ // "$0.0421", "$0.50", "$12.40". Sub-cent amounts keep four decimals.
44
+ function usd(micro) {
45
+ if (!isMicro(micro)) return '';
46
+ const neg = micro < 0;
47
+ const a = Math.abs(micro);
48
+ const text = a % MICRO_PER_CENT === 0
49
+ ? (a / MICRO_PER_USD).toFixed(2)
50
+ : (Math.round(a / 100) / 10000).toFixed(4);
51
+ return `${neg ? '−' : ''}$${text}`;
52
+ }
53
+
54
+ // A balance: two decimals, rounded down.
55
+ function balanceUsd(micro) {
56
+ if (!isMicro(micro)) return '';
57
+ const cents = Math.floor(micro / MICRO_PER_CENT);
58
+ return `$${(cents / 100).toFixed(2)}`;
59
+ }
60
+
61
+ function sizeText(bytes) {
62
+ const n = Number(bytes) || 0;
63
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
64
+ if (n >= 1024) return `${Math.round(n / 1024)} KB`;
65
+ return `${n} B`;
66
+ }
67
+
68
+ // Which page the address names. ?request=N is the waiting page or the
69
+ // result; ?view=balance the balance; otherwise one of the three steps.
70
+ function routeOf(search) {
71
+ const q = new URLSearchParams(String(search || ''));
72
+ const request = q.get('request');
73
+ if (request && /^\d{1,18}$/.test(request)) return { view: 'request', id: request };
74
+ if (q.get('view') === 'balance') return { view: 'balance', all: q.get('all') === '1' };
75
+ const step = STEPS.includes(q.get('step')) ? q.get('step') : 'pick';
76
+ const from = q.get('from');
77
+ return { view: 'step', step, from: from && /^\d{1,18}$/.test(from) ? from : null };
78
+ }
79
+
80
+ // Says no to a file before it is sent, in the same words the server would.
81
+ function checkFile(file, count) {
82
+ if (count >= MAX_FILES) return `Send up to ${MAX_FILES} files at a time.`;
83
+ const type = String(file && file.type || '');
84
+ if (type !== PDF_TYPE && !IMAGE_TYPES.includes(type)) return 'Pictures (PNG, JPG, GIF, WebP) or a PDF only.';
85
+ const max = type === PDF_TYPE ? MAX_PDF_BYTES : MAX_IMAGE_BYTES;
86
+ if ((Number(file.size) || 0) > max) return type === PDF_TYPE ? 'PDFs up to 8 MB.' : 'Pictures up to 4 MB.';
87
+ return null;
88
+ }
89
+
90
+ function isImage(contentType) { return IMAGE_TYPES.includes(String(contentType || '')); }
91
+
92
+ // The step header: "1 Pick ✓", "2 Describe", "3 Make".
93
+ function stepMarks(step) {
94
+ const at = STEPS.indexOf(step);
95
+ return [['1', 'Pick'], ['2', 'Describe'], ['3', 'Make']].map(([n, label], i) => ({
96
+ text: `${n} ${label}${i < at ? ' ✓' : ''}`,
97
+ state: i < at ? 'done' : (i === at ? 'now' : 'later'),
98
+ }));
99
+ }
100
+
101
+ // The prompt as one quoted line for step 3's summary row.
102
+ function quoteLine(prompt, max) {
103
+ const t = String(prompt || '').replace(/\s+/g, ' ').trim();
104
+ const cap = max || 64;
105
+ return `“${t.length > cap ? `${t.slice(0, cap - 1).trimEnd()}…` : t}”`;
106
+ }
107
+
108
+ // The own key the server will run on: OpenRouter first, as keys.chooseKey
109
+ // prefers it (it reports its cost). null when none is saved.
110
+ function ownKey(keys) {
111
+ const list = (keys && keys.keys) || [];
112
+ const set = (p) => list.find((k) => k && k.provider === p && k.set);
113
+ const k = set('openrouter') || set('gemini');
114
+ if (!k) return null;
115
+ return k.provider === 'openrouter'
116
+ ? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
117
+ : { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 };
118
+ }
119
+
120
+ // Step 3's two cards. balance is GET /ai-studio/balance's body.
121
+ function payChoices(keys, balance) {
122
+ const own = ownKey(keys);
123
+ const b = balance || {};
124
+ const avail = b.balance ? b.balance.available_micro : null;
125
+ const max = isMicro(b.max_cost_micro) ? b.max_cost_micro : null;
126
+ const payments = !!b.balance;
127
+ return {
128
+ own,
129
+ payments,
130
+ setAside: max === null ? '' : usd(max),
131
+ left: payments ? `${balanceUsd(avail)} left` : '',
132
+ short: payments && max !== null && avail < max,
133
+ };
134
+ }
135
+
136
+ // The waiting page's three lines, from the request's status and how long it
137
+ // has run.
138
+ function waitSteps(status, elapsedSec, fileCount) {
139
+ const n = Number(fileCount) || 0;
140
+ const files = n === 1 ? 'Read your file' : `Read your ${n} files`;
141
+ const running = status === 'running';
142
+ const answered = running && elapsedSec >= ANSWER_AFTER_SEC;
143
+ return [
144
+ { text: files, state: running ? 'done' : 'now' },
145
+ { text: 'Wrote the answer', state: answered ? 'done' : (running ? 'now' : 'later') },
146
+ { text: 'Drawing the pictures', state: answered ? 'now' : 'later' },
147
+ ];
148
+ }
149
+
150
+ function clock(sec) {
151
+ const s = Math.max(0, Math.floor(Number(sec) || 0));
152
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
153
+ }
154
+
155
+ function isFinished(status) { return FINISHED.includes(status); }
156
+
157
+ // The receipt on the results page: rows of [label, value] and a note.
158
+ // (The stored request does not name which of the builder's keys ran.)
159
+ // key_source 'ours' is the balance; 'own' with a reported cost is an
160
+ // OpenRouter key; 'own' with none is a Gemini key (Google reports no price).
161
+ // A request that did not finish has no receipt: its error line already says
162
+ // what was (not) charged.
163
+ function receipt(req, multiplier) {
164
+ const r = req || {};
165
+ if (r.status !== 'done') return { rows: [], note: '' };
166
+ if (r.key_source === 'ours' && isMicro(r.price_micro)) {
167
+ const pct = Number(multiplier) > 1 ? Math.round((Number(multiplier) - 1) * 100) : null;
168
+ return {
169
+ rows: [
170
+ ['Paid with', 'your balance'],
171
+ ['OpenRouter', usd(r.provider_cost_micro)],
172
+ [pct === null ? 'Ours' : `Our ${pct}%`, usd(r.markup_micro)],
173
+ ['You paid', usd(r.price_micro)],
174
+ ],
175
+ note: '',
176
+ };
177
+ }
178
+ if (r.key_source === 'own' && isMicro(r.provider_cost_micro)) {
179
+ return {
180
+ rows: [['Paid with', 'your own OpenRouter key'], ['Cost', usd(r.provider_cost_micro)]],
181
+ note: 'OpenRouter bills you for it directly. We charged nothing.',
182
+ };
183
+ }
184
+ if (r.key_source === 'own') {
185
+ return {
186
+ rows: [['Paid with', 'your own Gemini key'], ['Cost', 'not reported']],
187
+ note: "Google doesn't tell us the price on your own key, so check your Google bill. We charged nothing.",
188
+ };
189
+ }
190
+ return { rows: [], note: '' };
191
+ }
192
+
193
+ // "Same 3 files, paid the same way."
194
+ function againLine(uploadCount) {
195
+ const n = Number(uploadCount) || 0;
196
+ return `Change the words and make it again. Same ${n === 1 ? 'file' : `${n} files`}, paid the same way.`;
197
+ }
198
+
199
+ // The month's summary of the ledger rows that fall in `now`'s month.
200
+ function monthSummary(ledger, now) {
201
+ const d = now instanceof Date ? now : new Date();
202
+ const inMonth = (row) => {
203
+ const t = new Date(row.created_at);
204
+ return !Number.isNaN(t.getTime()) && t.getFullYear() === d.getFullYear() && t.getMonth() === d.getMonth();
205
+ };
206
+ const rows = (ledger || []).filter(inMonth);
207
+ const sum = (kind) => rows.filter((r) => r.kind === kind).reduce((a, r) => a + (Number(r.amount_micro) || 0), 0);
208
+ const count = (kind) => rows.filter((r) => r.kind === kind).length;
209
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
210
+ const lines = [];
211
+ if (count('topup')) lines.push({ label: 'Added', value: `+${usd(sum('topup'))}`, tone: 'ok' });
212
+ if (count('capture')) lines.push({ label: `Spent on ${plural(count('capture'), 'request', 'requests')}`, value: usd(-sum('capture')), tone: '' });
213
+ if (count('release')) lines.push({ label: `Money back from ${plural(count('release'), 'failed request', 'failed requests')}`, value: `${usd(sum('release'))} returned`, tone: 'ok' });
214
+ if (count('own_key')) lines.push({ label: `On your own keys, ${plural(count('own_key'), 'request', 'requests')}`, value: 'free', tone: 'dim' });
215
+ return { month: d.toLocaleString('en', { month: 'long' }), lines };
216
+ }
217
+
218
+ // One line of the full ledger. `prompts` maps request id to its prompt.
219
+ function ledgerLine(row, prompts) {
220
+ const r = row || {};
221
+ const what = (r.request_id && prompts && prompts[String(r.request_id)])
222
+ ? quoteLine(prompts[String(r.request_id)], 40).slice(1, -1)
223
+ : 'A request';
224
+ const amt = Number(r.amount_micro) || 0;
225
+ switch (r.kind) {
226
+ case 'topup': return { what: 'Top up', detail: String(r.note || '').replace(/^card charged /, 'card '), value: `+${usd(amt)}`, tone: 'ok' };
227
+ case 'hold': return { what, detail: 'set aside while it ran', value: usd(amt), tone: 'dim' };
228
+ case 'capture': return { what, detail: `${usd(Number(r.provider_cost_micro) || 0)} + ${usd(Number(r.markup_micro) || 0)}`, value: usd(-amt), tone: '' };
229
+ case 'release': return { what, detail: 'failed, all money back', value: `${usd(amt)} returned`, tone: 'ok' };
230
+ case 'own_key': return { what, detail: 'your own key', value: 'free', tone: 'dim' };
231
+ default: return { what, detail: '', value: usd(amt), tone: '' };
232
+ }
233
+ }
234
+
235
+ // The model's reply as paragraphs, **bold** kept, everything else escaped by
236
+ // the hall's ONE escaper, handed in (window.OTB.escapeHtml in the page).
237
+ function replyHtml(md, escape) {
238
+ const text = String(md || '').replace(/\r\n/g, '\n').trim();
239
+ if (!text || typeof escape !== 'function') return '';
240
+ return text.split(/\n{2,}/).map((para) => {
241
+ const safe = escape(para.trim()).replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>');
242
+ return `<p>${safe}</p>`;
243
+ }).join('');
244
+ }
245
+
246
+ // The home's AI action line: the newest request, while it is being made or
247
+ // when it finished in the last day, is what the line points at.
248
+ function homeLine(requests, now) {
249
+ const newest = (requests || [])[0];
250
+ const t = now instanceof Date ? now.getTime() : Date.now();
251
+ if (newest && (newest.status === 'queued' || newest.status === 'running')) {
252
+ return { detail: 'one is being made now', href: `/builders/ai-studio?request=${newest.id}` };
253
+ }
254
+ const fin = newest && newest.finished_at ? new Date(newest.finished_at).getTime() : NaN;
255
+ if (newest && newest.status === 'done' && t - fin < 24 * 3600 * 1000) {
256
+ return { detail: 'your newest pictures are ready', href: `/builders/ai-studio?request=${newest.id}` };
257
+ }
258
+ return { detail: 'sketches in, ideas and pictures out', href: '/builders/ai-studio' };
259
+ }
260
+
261
+ const lib = {
262
+ MAX_FILES, MAX_IMAGE_BYTES, MAX_PDF_BYTES, IMAGE_TYPES, PDF_TYPE, STEPS, TOPUP_CHOICES, ANSWER_AFTER_SEC,
263
+ usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, payChoices,
264
+ waitSteps, clock, isFinished, receipt, againLine, monthSummary, ledgerLine, replyHtml, homeLine,
265
+ };
266
+ if (typeof module !== 'undefined' && module.exports) module.exports = lib;
267
+ if (root) root.OTBAiStudioLib = lib;
268
+ })(typeof window !== 'undefined' ? window : null);