@bongos/core 1.23.21 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (457) hide show
  1. package/.bongos-core.json +983 -428
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +6 -3
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +94 -0
  27. package/clients/bongos-client/index.cjs +94 -0
  28. package/clients/bongos-client/index.d.ts +154 -4
  29. package/clients/bongos-client/index.mjs +94 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
  33. package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
  34. package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
  35. package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
  36. package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
  37. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  38. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  39. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  40. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  41. package/docs/adr/README.md +10 -1
  42. package/docs/api/openapi.json +8619 -5885
  43. package/docs/api-reference.md +68 -9
  44. package/docs/architecture.md +37 -1
  45. package/docs/canonical-permissions.md +1 -1
  46. package/docs/copy-inventory.md +420 -337
  47. package/docs/copy-registry.json +1321 -500
  48. package/docs/file-map.md +4 -0
  49. package/docs/module-api-changelog.md +7 -0
  50. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  51. package/docs/onboarding/diagrams/README.md +1 -1
  52. package/docs/onboarding/drachmae-and-karma.md +15 -15
  53. package/docs/onboarding/primer.md +2 -2
  54. package/docs/page-inventory.json +55 -6
  55. package/docs/page-readings.json +1185 -1011
  56. package/docs/recipes/google-sign-in.md +60 -0
  57. package/docs/recipes/ops-gotchas.md +1 -1
  58. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  59. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  60. package/migrations/core_283_payout_approvals.sql +183 -0
  61. package/migrations/core_284_payout_gates.sql +179 -0
  62. package/migrations/core_285_task_needs.sql +74 -0
  63. package/modules/ai-studio/CLAUDE.md +15 -0
  64. package/modules/ai-studio/billing.js +67 -0
  65. package/modules/ai-studio/context.js +29 -0
  66. package/modules/ai-studio/db.js +172 -0
  67. package/modules/ai-studio/gemini.js +75 -0
  68. package/modules/ai-studio/generate.js +119 -0
  69. package/modules/ai-studio/keys.js +73 -0
  70. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  71. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  72. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  73. package/modules/ai-studio/module.json +26 -0
  74. package/modules/ai-studio/openrouter.js +90 -0
  75. package/modules/ai-studio/payments.js +105 -0
  76. package/modules/ai-studio/pricing.js +92 -0
  77. package/modules/ai-studio/routes/ai-studio.js +252 -0
  78. package/modules/ai-studio/studio.js +121 -0
  79. package/modules/autonomy/fence.js +64 -1
  80. package/modules/builder-settings/builder-needs.js +27 -1
  81. package/modules/copy-desk/routes/copy-desk.js +5 -0
  82. package/modules/economy/achievements.js +22 -10
  83. package/modules/economy/cost-classify.js +9 -0
  84. package/modules/economy/cost-write-guard.js +52 -0
  85. package/modules/economy/cost.js +2 -1
  86. package/modules/economy/credits.js +18 -15
  87. package/modules/economy/goal-reward.js +161 -0
  88. package/modules/economy/reward.js +4 -0
  89. package/modules/economy/routes/cost.js +33 -7
  90. package/modules/government/catalog.js +6 -0
  91. package/modules/government/db.js +108 -0
  92. package/modules/government/docket.js +36 -0
  93. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  94. package/modules/government/pollers/board-expiry.js +108 -20
  95. package/modules/government/protected-surfaces.json +1 -0
  96. package/modules/government/routes/government.js +50 -18
  97. package/modules/government/routing.js +39 -5
  98. package/modules/government/waiting.js +74 -0
  99. package/modules/grading/grader-subagent.js +30 -11
  100. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  101. package/modules/hall-ui/public/ai-studio.css +193 -0
  102. package/modules/hall-ui/public/ai-studio.html +98 -0
  103. package/modules/hall-ui/public/ai-studio.js +556 -0
  104. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  105. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  106. package/modules/hall-ui/public/blockers-lib.js +15 -1
  107. package/modules/hall-ui/public/blockers.html +8 -0
  108. package/modules/hall-ui/public/blockers.js +1 -1
  109. package/modules/hall-ui/public/city.html +15 -15
  110. package/modules/hall-ui/public/claim-action.js +3 -0
  111. package/modules/hall-ui/public/collab.html +12 -0
  112. package/modules/hall-ui/public/craft-mode.js +4 -1
  113. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  114. package/modules/hall-ui/public/drachmae.html +1 -1
  115. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  116. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  117. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  118. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  119. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  120. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  121. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  122. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  123. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  124. package/modules/hall-ui/public/goal-inbox.js +1 -1
  125. package/modules/hall-ui/public/goals-render.js +4 -0
  126. package/modules/hall-ui/public/goals.html +8 -0
  127. package/modules/hall-ui/public/gov-living.js +6 -2
  128. package/modules/hall-ui/public/hall-ask.css +5 -0
  129. package/modules/hall-ui/public/hall-ask.js +39 -3
  130. package/modules/hall-ui/public/notes-panel.css +20 -0
  131. package/modules/hall-ui/public/notes-panel.js +146 -0
  132. package/modules/hall-ui/public/project-settings.html +1 -1
  133. package/modules/hall-ui/public/project-settings.js +7 -5
  134. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  135. package/modules/hall-ui/public/settings.html +15 -0
  136. package/modules/hall-ui/public/street-deck.js +12 -3
  137. package/modules/hall-ui/public/studio.css +90 -8
  138. package/modules/hall-ui/public/studio.html +39 -10
  139. package/modules/hall-ui/public/studio.js +148 -18
  140. package/modules/hall-ui/public/task.html +2 -0
  141. package/modules/hall-ui/public/task.js +4 -0
  142. package/modules/hall-ui/public/tweak-editor.js +12 -3
  143. package/modules/hall-ui/public/welcome-boot.css +42 -6
  144. package/modules/hall-ui/public/welcome-boot.js +20 -6
  145. package/modules/hall-ui/public/work.js +20 -0
  146. package/modules/hall-ui/records/ai-studio.md +36 -0
  147. package/modules/hall-ui/records/blockers-people.md +24 -0
  148. package/modules/lifecycle/db-analytics.js +10 -3
  149. package/modules/lifecycle/db-claims.js +32 -4
  150. package/modules/lifecycle/db-goals.js +48 -18
  151. package/modules/lifecycle/db-shared.js +9 -0
  152. package/modules/lifecycle/db-tasks.js +15 -6
  153. package/modules/lifecycle/db-versions.js +21 -8
  154. package/modules/lifecycle/done-when.js +23 -1
  155. package/modules/lifecycle/lifecycle.js +5 -0
  156. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  157. package/modules/lifecycle/module.json +2 -1
  158. package/modules/lifecycle/notes.js +219 -0
  159. package/modules/lifecycle/page-notice.js +124 -0
  160. package/modules/lifecycle/page-tweak-artist.js +18 -1
  161. package/modules/lifecycle/publish-reconciler.js +67 -20
  162. package/modules/lifecycle/routes/claims.js +5 -2
  163. package/modules/lifecycle/routes/goals.js +3 -1
  164. package/modules/lifecycle/routes/notes.js +91 -0
  165. package/modules/lifecycle/routes/project-settings.js +43 -35
  166. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  167. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  168. package/modules/lifecycle/routes/tasks.js +12 -12
  169. package/modules/lifecycle/ship-assurance.js +4 -1
  170. package/modules/lifecycle/ship-card.js +8 -2
  171. package/modules/lifecycle/task-needs-gate.js +70 -0
  172. package/modules/market-watch/CLAUDE.md +15 -0
  173. package/modules/market-watch/db.js +340 -0
  174. package/modules/market-watch/key.js +58 -0
  175. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  176. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  177. package/modules/market-watch/module.json +26 -0
  178. package/modules/market-watch/pollers/hub-seed.js +54 -0
  179. package/modules/market-watch/pollers/weekly-run.js +100 -0
  180. package/modules/market-watch/research.js +350 -0
  181. package/modules/market-watch/run.js +245 -0
  182. package/modules/market-watch/seed/market-data.json +38 -0
  183. package/modules/market-watch/seed/partners-data.json +497 -0
  184. package/modules/market-watch/seed.js +252 -0
  185. package/modules/market-watch/store.js +230 -0
  186. package/modules/memory/routes/memory.js +14 -2
  187. package/modules/memory/routes/search.js +8 -2
  188. package/modules/onboarding/howto-moments.js +8 -1
  189. package/modules/platform-identity/account-logins.js +192 -0
  190. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  191. package/modules/platform-identity/platform-identity.js +5 -4
  192. package/modules/platform-identity/routes/my-projects.js +6 -0
  193. package/modules/platform-identity/routes/profile.js +18 -0
  194. package/modules/platform-identity/routes/public-profile.js +10 -1
  195. package/modules/platform-identity/routes/sso.js +26 -1
  196. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  197. package/modules/provisioning/credential-preflight.js +13 -3
  198. package/modules/provisioning/pollers/app-liveness.js +25 -5
  199. package/modules/provisioning/routes/provisioning.js +1 -1
  200. package/modules/provisioning/starter-bundles.js +8 -2
  201. package/modules/public-landing/public/account.html +10 -2
  202. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  203. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  204. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  205. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  206. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  207. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  208. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  209. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  210. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  211. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  212. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  213. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  214. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  215. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  216. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  217. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  218. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  219. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  220. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  221. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  222. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  223. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  224. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  225. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  226. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  227. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  228. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  229. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  230. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  231. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  232. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  233. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  234. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  235. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  236. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  237. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  238. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  239. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  240. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  241. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  242. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  243. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  244. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  245. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  246. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  247. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  248. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  249. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  250. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  251. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  252. package/modules/public-landing/public/community.html +1 -1
  253. package/modules/public-landing/public/connections.html +2 -2
  254. package/modules/public-landing/public/guild.html +1 -1
  255. package/modules/public-landing/public/invitations.html +2 -2
  256. package/modules/public-landing/public/projects.html +5 -5
  257. package/modules/raise/views/nda.html +2 -0
  258. package/modules/specialities/db.js +32 -1
  259. package/modules/specialities/routes/specialities.js +10 -6
  260. package/modules/status-ui/public/index.html +1 -1
  261. package/package-lock.json +2 -2
  262. package/package.json +1 -1
  263. package/release-notes.json +286 -0
  264. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  265. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  266. package/scripts/bongos/claim.js +22 -1
  267. package/scripts/bongos/db-backup-nightly.sh +29 -0
  268. package/scripts/bongos/deploy-divergence.js +7 -0
  269. package/scripts/bongos/doc-cli-guard.js +6 -0
  270. package/scripts/bongos/fitness.js +3 -2
  271. package/scripts/bongos/gen-diagrams.js +3 -3
  272. package/scripts/bongos/lib.sh +4 -5
  273. package/scripts/bongos/migration-namespace.js +13 -1
  274. package/scripts/bongos/plain-cards.js +4 -0
  275. package/scripts/bongos/provision-github-setup.js +3 -1
  276. package/scripts/bongos/provision-units.js +6 -1
  277. package/scripts/bongos/publish-manifest.js +4 -0
  278. package/scripts/bongos/release.js +2 -0
  279. package/scripts/bongos/run-unit-tests.js +268 -82
  280. package/scripts/bongos/seed-tasks.example.json +56 -0
  281. package/scripts/bongos/seed-tasks.js +222 -0
  282. package/scripts/bongos/seed-verify-criteria.js +1 -2
  283. package/scripts/bongos/ship-finish.js +2 -0
  284. package/scripts/bongos/ship-resume.js +2 -0
  285. package/scripts/bongos/strand-lease.js +250 -0
  286. package/scripts/bongos/strand-watch.js +129 -12
  287. package/scripts/bongos/version-literal-guard.js +2 -2
  288. package/scripts/discord/post.js +4 -3
  289. package/scripts/gds/seed-tasks.js +7 -0
  290. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  291. package/scripts/hall-preview/server.js +21 -0
  292. package/src/bongos/auth-config.js +29 -1
  293. package/src/bongos/auth-discord.js +3 -3
  294. package/src/bongos/auth-github.js +3 -6
  295. package/src/bongos/auth-google.js +217 -0
  296. package/src/bongos/device-approval.js +140 -0
  297. package/src/bongos/llm-pricing.js +17 -1
  298. package/src/bongos/metrics.js +48 -2
  299. package/src/bongos/middleware/rate-limit.js +30 -11
  300. package/src/bongos/module-scope-map.js +16 -1
  301. package/src/bongos/optimizer.js +15 -1
  302. package/src/bongos/payout-approvals.js +342 -0
  303. package/src/bongos/payout-gates.js +364 -0
  304. package/src/bongos/prelaunch-gate.js +7 -9
  305. package/src/bongos/prelaunch-switch.js +20 -0
  306. package/src/bongos/route-rank-check.js +3 -0
  307. package/src/bongos/routes/auth-google.js +246 -0
  308. package/src/bongos/routes/auth.js +38 -1
  309. package/src/bongos/routes/backup.js +33 -9
  310. package/src/bongos/routes/builders.js +57 -30
  311. package/src/bongos/routes/github.js +9 -0
  312. package/src/bongos/routes/me.js +18 -0
  313. package/src/bongos/routes/my-payouts.js +133 -0
  314. package/src/bongos/routes/payout-approvals.js +312 -0
  315. package/src/bongos/routes/payout-gates.js +179 -0
  316. package/src/bongos/routes.js +15 -1
  317. package/src/bongos/secret-box.js +18 -3
  318. package/src/bongos/serve-internal.js +47 -19
  319. package/src/bongos/sign-in-pages.js +138 -0
  320. package/src/bongos/task-needs.js +146 -0
  321. package/src/bongos-downloads.js +18 -1
  322. package/src/instance-config.js +28 -0
  323. package/src/module-api.js +14 -1
  324. package/src/platform-server.js +27 -1
  325. package/tests/account_logins.mjs +326 -0
  326. package/tests/account_logins_db.mjs +161 -0
  327. package/tests/ai_studio_billing.mjs +253 -0
  328. package/tests/ai_studio_generate.mjs +282 -0
  329. package/tests/ai_studio_keys.mjs +159 -0
  330. package/tests/ai_studio_requests.mjs +319 -0
  331. package/tests/auth_backchannel_logout_route.mjs +201 -0
  332. package/tests/auto_satisfy_criteria.mjs +10 -1
  333. package/tests/autobongos_tweak_lane.mjs +144 -0
  334. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  335. package/tests/backfill_subagent_rewards.mjs +17 -1
  336. package/tests/backup_trigger_lock.mjs +119 -0
  337. package/tests/batched_writes_parity.mjs +340 -0
  338. package/tests/blockers_people.mjs +198 -0
  339. package/tests/bounded_rate_maps.mjs +184 -0
  340. package/tests/claimable_blocker_status.mjs +1 -1
  341. package/tests/claims_batch_routes.mjs +421 -0
  342. package/tests/conductor_main_e2e.mjs +4 -0
  343. package/tests/config_read_cache.mjs +171 -0
  344. package/tests/copy_desk_page_editor.mjs +12 -0
  345. package/tests/cost_write_guard.mjs +200 -0
  346. package/tests/craft_achievements.mjs +33 -10
  347. package/tests/criterion_goal_attach.mjs +1 -1
  348. package/tests/db_backup_nightly.mjs +73 -1
  349. package/tests/deploy_divergence_line.mjs +7 -0
  350. package/tests/effective_visibility_predicate.mjs +12 -0
  351. package/tests/fitness.mjs +19 -0
  352. package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
  353. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  354. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  355. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  356. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  357. package/tests/github_one_trip.mjs +34 -0
  358. package/tests/github_repo_visibility.mjs +20 -0
  359. package/tests/goal_achievement.mjs +30 -1
  360. package/tests/goal_achievement_reward.mjs +205 -0
  361. package/tests/goal_archive_disposition.mjs +5 -5
  362. package/tests/goal_sort_staleness.mjs +2 -1
  363. package/tests/goal_tier.mjs +11 -1
  364. package/tests/google_fonts_nonblocking.mjs +84 -0
  365. package/tests/google_sign_in.mjs +401 -0
  366. package/tests/government_board_expiry.mjs +94 -0
  367. package/tests/government_protected_surfaces.mjs +1 -1
  368. package/tests/government_routes.mjs +146 -23
  369. package/tests/grader.mjs +31 -0
  370. package/tests/hall_ai_studio.mjs +347 -0
  371. package/tests/hall_city.mjs +8 -0
  372. package/tests/hall_page_gate_map.mjs +3 -0
  373. package/tests/hall_studio_home.mjs +61 -2
  374. package/tests/home_attention_cards.mjs +1 -1
  375. package/tests/hook_shell_tools.mjs +8 -0
  376. package/tests/hooks_drain.mjs +1 -1
  377. package/tests/host_topology_skips.mjs +2 -0
  378. package/tests/howto_moments.mjs +17 -0
  379. package/tests/human_signal.mjs +141 -0
  380. package/tests/install_script_reader.mjs +51 -0
  381. package/tests/integration_fixture_ids.mjs +82 -0
  382. package/tests/invite_expiry_db.mjs +13 -1
  383. package/tests/lib_sh_resolution.mjs +0 -4
  384. package/tests/market_watch_data.mjs +244 -0
  385. package/tests/market_watch_data_db.mjs +308 -0
  386. package/tests/market_watch_module.mjs +114 -0
  387. package/tests/market_watch_run.mjs +503 -0
  388. package/tests/metrics.mjs +124 -0
  389. package/tests/migration_namespace.mjs +14 -0
  390. package/tests/module_api.mjs +2 -0
  391. package/tests/module_loader.mjs +1 -1
  392. package/tests/module_skills_hook.mjs +69 -0
  393. package/tests/notes.mjs +182 -0
  394. package/tests/own_scope_guard.mjs +5 -0
  395. package/tests/page_tweak_marker_sql.mjs +76 -0
  396. package/tests/page_tweak_no_block.mjs +135 -0
  397. package/tests/page_tweak_notice.mjs +103 -0
  398. package/tests/payout_approvals.mjs +253 -0
  399. package/tests/payout_approvals_db.mjs +106 -0
  400. package/tests/payout_gates.mjs +322 -0
  401. package/tests/payout_gates_db.mjs +113 -0
  402. package/tests/planning_session_skill.mjs +13 -3
  403. package/tests/project_settings_page.mjs +29 -5
  404. package/tests/project_settings_spend_cap_route.mjs +235 -0
  405. package/tests/promotion_sweep.mjs +29 -1
  406. package/tests/provision.mjs +3 -2
  407. package/tests/provisioning_app_status.mjs +18 -0
  408. package/tests/provisioning_credential_preflight.mjs +53 -0
  409. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  410. package/tests/public_landing_projects.mjs +37 -0
  411. package/tests/publish_reconciler_overlap.mjs +60 -0
  412. package/tests/rank_change_broadcast_port.mjs +151 -0
  413. package/tests/seed_tasks.mjs +176 -0
  414. package/tests/seed_verify_criteria.mjs +2 -6
  415. package/tests/session_start_freshness.mjs +66 -1
  416. package/tests/shell_hooks.mjs +209 -0
  417. package/tests/ship_assurance.mjs +28 -0
  418. package/tests/ship_card.mjs +20 -5
  419. package/tests/site_classified_once.mjs +142 -0
  420. package/tests/speciality_db_shape.mjs +23 -0
  421. package/tests/speciality_session_skills.mjs +48 -2
  422. package/tests/strand_lease.mjs +249 -0
  423. package/tests/strand_watch.mjs +58 -0
  424. package/tests/task_criteria_routes.mjs +275 -0
  425. package/tests/task_criteria_unlink_db.mjs +123 -0
  426. package/tests/task_needs.mjs +233 -0
  427. package/tests/task_needs_db.mjs +110 -0
  428. package/tests/unit_runner_serial.mjs +266 -43
  429. package/tests/version_close_route.mjs +1 -1
  430. package/tests/version_literals.mjs +5 -7
  431. package/tests/welcome_boot.mjs +16 -5
  432. package/tests/welcome_boot_browser.mjs +36 -4
  433. package/docs/recipes/bongos-cli-release.md +0 -66
  434. package/scripts/bongos/backfill-disciplines.js +0 -144
  435. package/scripts/bongos/backfill-session-earnings.js +0 -123
  436. package/scripts/bongos/backfill-session-types.js +0 -87
  437. package/scripts/bongos/build-bongos.js +0 -94
  438. package/scripts/bongos/cleanup-orphans.js +0 -74
  439. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  440. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  441. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  442. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  443. package/scripts/bongos/smoke-batch-release.sh +0 -316
  444. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  445. package/scripts/bongos/smoke-dependencies.sh +0 -143
  446. package/scripts/gds/backfill-disciplines.js +0 -7
  447. package/scripts/gds/backfill-session-earnings.js +0 -7
  448. package/scripts/gds/backfill-session-types.js +0 -7
  449. package/scripts/gds/cleanup-orphans.js +0 -7
  450. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  451. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  452. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  453. package/scripts/gds/smoke-batch-claim.sh +0 -8
  454. package/scripts/gds/smoke-batch-release.sh +0 -8
  455. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  456. package/scripts/gds/smoke-dependencies.sh +0 -8
  457. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -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 };
@@ -183,18 +183,23 @@ function runSubagent({ prompt, opts = {} }) {
183
183
  // agentic Read turns re-opening the same changed files. Read them ONCE here
184
184
  // and hand every worker the post-change contents inline (fenced, untrusted),
185
185
  // so a worker's Read tool is only needed for files beyond the bundle.
186
+ //
187
+ // task 1004932: the bundle rides in EVERY worker prompt, so its total is paid
188
+ // once per worker; 120k chars made it the largest fixed input of a grade.
186
189
  const CONTEXT_BUNDLE_MAX_FILES = 24;
187
- const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 120_000;
190
+ const CONTEXT_BUNDLE_MAX_TOTAL_CHARS = 60_000;
188
191
  const CONTEXT_BUNDLE_MAX_FILE_CHARS = 16_000;
189
192
  // Binary-ish extensions never worth inlining (the diff already names them).
190
193
  const CONTEXT_BUNDLE_SKIP_RE = /\.(?:png|jpe?g|gif|webp|ico|svgz|woff2?|ttf|eot|zip|gz|tgz|tar|pdf|mp[34]|wav|ogg|bin|exe|dll|node)$/i;
191
194
 
192
- // Added lines per file, from the unified diff. Only lines long enough to be a
193
- // meaningful fingerprint (a bare `}` or `});` matches anywhere) — used to prove
194
- // a truncated bundle prefix actually SHOWS the change (task 1002905).
195
+ // One pass over a unified diff, per file: how many lines it adds (a relevance
196
+ // weight), and the added lines long enough to be a meaningful fingerprint (a bare
197
+ // `}` or `});` matches anywhere) — used to prove a truncated bundle prefix
198
+ // actually SHOWS the change (task 1002905).
195
199
  const BUNDLE_EVIDENCE_MIN_CHARS = 12;
196
- function diffAddedLinesByFile(diff) {
197
- const byFile = new Map();
200
+ function scanDiff(diff) {
201
+ const evidence = new Map();
202
+ const added = new Map();
198
203
  let cur = null;
199
204
  let prevWasOldHeader = false;
200
205
  for (const line of String(diff || '').split('\n')) {
@@ -209,12 +214,26 @@ function diffAddedLinesByFile(diff) {
209
214
  prevWasOldHeader = line.startsWith('--- ');
210
215
  if (prevWasOldHeader || line.startsWith('diff --git')) continue;
211
216
  if (!cur || !line.startsWith('+')) continue;
217
+ added.set(cur, (added.get(cur) || 0) + 1);
212
218
  const t = line.slice(1).trim();
213
219
  if (t.length < BUNDLE_EVIDENCE_MIN_CHARS) continue;
214
- if (!byFile.has(cur)) byFile.set(cur, []);
215
- byFile.get(cur).push(t);
220
+ if (!evidence.has(cur)) evidence.set(cur, []);
221
+ evidence.get(cur).push(t);
216
222
  }
217
- return byFile;
223
+ return { evidence, added };
224
+ }
225
+ function diffAddedLinesByFile(diff) { return scanDiff(diff).evidence; }
226
+
227
+ // The order the bundle is filled in (task 1004932): the files the diff adds the
228
+ // most lines to first, so when the total cap bites it is the lightly touched
229
+ // files (a one-line doc pointer, a generated index) that fall to skipped[] for
230
+ // the worker to Read, not the heart of the change. Stable: ties, and every file
231
+ // when there is no diff, keep the caller's order.
232
+ function orderByChange(changedFiles, added) {
233
+ return changedFiles
234
+ .map((file, i) => ({ file, i, n: added.get(file) || 0 }))
235
+ .sort((a, b) => (b.n - a.n) || (a.i - b.i))
236
+ .map((x) => x.file);
218
237
  }
219
238
 
220
239
  // Pure-ish (fs reads only, injectable): returns { files: [{file, content,
@@ -241,8 +260,8 @@ function buildContextBundle({ changedFiles, cwd, diff, fsImpl = fs } = {}) {
241
260
  // exactly the pre-change state). A prefix that contains none of the change is
242
261
  // worse than no bundle at all, so refuse it: skipped[] already tells the worker
243
262
  // to Read the file.
244
- const addedByFile = diffAddedLinesByFile(diff);
245
- for (const rel of (Array.isArray(changedFiles) ? changedFiles : [])) {
263
+ const { evidence: addedByFile, added } = scanDiff(diff);
264
+ for (const rel of orderByChange(Array.isArray(changedFiles) ? changedFiles : [], added)) {
246
265
  if (files.length >= CONTEXT_BUNDLE_MAX_FILES || total >= CONTEXT_BUNDLE_MAX_TOTAL_CHARS) {
247
266
  skipped.push(rel);
248
267
  continue;
@@ -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);