@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.220

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 (311) hide show
  1. package/README.md +2 -2
  2. package/package.json +18 -18
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +8 -1
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  8. package/src/lib/billing/ai-month-meter.js +89 -0
  9. package/src/lib/billing/ai-month-meter.js.map +1 -0
  10. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  11. package/src/lib/billing/ai-overage-close.js +4 -3
  12. package/src/lib/billing/ai-overage-close.js.map +1 -1
  13. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  14. package/src/lib/billing/ai-overage-cutover.js +1 -28
  15. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  16. package/src/lib/billing/ai-overage-ledger.js +1 -1
  17. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  18. package/src/lib/billing/ai-overage-standing.js +1 -1
  19. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  20. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  21. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  22. package/src/lib/components/ai-assist-provider.component.js +5 -4
  23. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  24. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  25. package/src/lib/components/ai-automation-zones.js +31 -0
  26. package/src/lib/components/ai-automation-zones.js.map +1 -0
  27. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  28. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  29. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  30. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  31. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  32. package/src/lib/components/ai-describe-button.component.js +4 -1
  33. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  34. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  35. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  36. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  37. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  38. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  39. package/src/lib/components/ai-job-plan.component.js +21 -4
  40. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  41. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  42. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  43. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  44. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  45. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  46. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  47. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  48. package/src/lib/components/ai-product-zones.d.ts +124 -0
  49. package/src/lib/components/ai-product-zones.js +32 -0
  50. package/src/lib/components/ai-product-zones.js.map +1 -0
  51. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  52. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  53. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  54. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  55. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  56. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  57. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  60. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  61. package/src/lib/components/assist-panel.component.js +2 -2
  62. package/src/lib/components/assist-panel.component.js.map +1 -1
  63. package/src/lib/components/assist-signals-page.component.js +2 -2
  64. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  65. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  66. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  67. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  68. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  69. package/src/lib/constants.d.ts +5 -0
  70. package/src/lib/constants.js +4 -0
  71. package/src/lib/constants.js.map +1 -1
  72. package/src/lib/declarations.js +2 -0
  73. package/src/lib/declarations.js.map +1 -1
  74. package/src/lib/declarations.server.js +24 -10
  75. package/src/lib/declarations.server.js.map +1 -1
  76. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  77. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  78. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  79. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  80. package/src/lib/insights/ai-figure-readers.js +2 -391
  81. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  82. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  83. package/src/lib/insights/ai-insight-digest.js +16 -4
  84. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  85. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  86. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  87. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  88. package/src/lib/jobs/ai-email-bindings.js +75 -42
  89. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  90. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  91. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  92. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  93. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  94. package/src/lib/jobs/ai-job-component-step.js +32 -9
  95. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  96. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  97. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  98. package/src/lib/jobs/ai-job-drafts.js +1 -1
  99. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  100. package/src/lib/jobs/ai-job-email-step.d.ts +5 -3
  101. package/src/lib/jobs/ai-job-email-step.js +11 -4
  102. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  104. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  105. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  107. package/src/lib/jobs/ai-job-form-step.js +60 -8
  108. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  109. package/src/lib/jobs/ai-job-generation.d.ts +22 -1
  110. package/src/lib/jobs/ai-job-generation.js +63 -3
  111. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  112. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  113. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  114. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  115. package/src/lib/jobs/ai-job-layout-step.js +46 -13
  116. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  117. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  118. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  119. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  120. package/src/lib/jobs/ai-job-page-step.js +7 -2
  121. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  122. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  123. package/src/lib/jobs/ai-job-plan-conformance.js +40 -1
  124. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  125. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  126. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  127. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  128. package/src/lib/jobs/ai-job-plan-step.d.ts +11 -2
  129. package/src/lib/jobs/ai-job-plan-step.js +49 -23
  130. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  131. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  132. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  133. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  134. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  135. package/src/lib/jobs/ai-job-products-step.js +54 -32
  136. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  137. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  138. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  139. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  140. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  141. package/src/lib/jobs/ai-job-site-step.js +6 -6
  142. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  143. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  144. package/src/lib/jobs/ai-job-template-step.js +73 -15
  145. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  146. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  147. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  148. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  149. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  150. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  151. package/src/lib/jobs/ai-jobs.js +1 -1
  152. package/src/lib/jobs/ai-jobs.js.map +1 -1
  153. package/src/lib/jobs/ai-workflow-records.d.ts +20 -11
  154. package/src/lib/jobs/ai-workflow-records.js +80 -43
  155. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  156. package/src/lib/model/ai-allotments.d.ts +27 -6
  157. package/src/lib/model/ai-allotments.js +54 -14
  158. package/src/lib/model/ai-allotments.js.map +1 -1
  159. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  160. package/src/lib/model/ai-automation-outline.js.map +1 -1
  161. package/src/lib/model/ai-build-plan.d.ts +57 -0
  162. package/src/lib/model/ai-build-plan.js +116 -7
  163. package/src/lib/model/ai-build-plan.js.map +1 -1
  164. package/src/lib/model/ai-job-creations.d.ts +55 -0
  165. package/src/lib/model/ai-job-creations.js +85 -0
  166. package/src/lib/model/ai-job-creations.js.map +1 -0
  167. package/src/lib/model/ai-page-job.d.ts +3 -2
  168. package/src/lib/model/ai-page-job.js +4 -3
  169. package/src/lib/model/ai-page-job.js.map +1 -1
  170. package/src/lib/model/ai-seo.d.ts +11 -51
  171. package/src/lib/model/ai-seo.js +0 -17
  172. package/src/lib/model/ai-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-inventory.js +1 -1
  174. package/src/lib/model/ai-site-inventory.js.map +1 -1
  175. package/src/lib/model/ai-site-job.d.ts +4 -2
  176. package/src/lib/model/ai-site-job.js +8 -4
  177. package/src/lib/model/ai-site-job.js.map +1 -1
  178. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  179. package/src/lib/model/ai-template-subjects.js +16 -2
  180. package/src/lib/model/ai-template-subjects.js.map +1 -1
  181. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  182. package/src/lib/model/ai-theme-proposal.js +1 -1
  183. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  184. package/src/lib/model/ai-tokens.js +1 -1
  185. package/src/lib/model/ai-tokens.js.map +1 -1
  186. package/src/lib/model/ai-usage-by-user.js +1 -1
  187. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  188. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  189. package/src/lib/model/ai-workflow-job.js +1 -2
  190. package/src/lib/model/ai-workflow-job.js.map +1 -1
  191. package/src/lib/plan-entitlements.d.ts +149 -0
  192. package/src/lib/plan-entitlements.js +168 -0
  193. package/src/lib/plan-entitlements.js.map +1 -0
  194. package/src/lib/plugin.js +1 -1
  195. package/src/lib/plugin.js.map +1 -1
  196. package/src/lib/providers/catalog.js +1 -1
  197. package/src/lib/providers/catalog.js.map +1 -1
  198. package/src/lib/providers/model-choice.js +1 -1
  199. package/src/lib/providers/model-choice.js.map +1 -1
  200. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  201. package/src/lib/runtime/ai-device-audit.js +1 -1
  202. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  203. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  204. package/src/lib/runtime/ai-doctrine-validators.js +178 -21
  205. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  206. package/src/lib/runtime/ai-doctrine.js +5 -5
  207. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  208. package/src/lib/runtime/ai-eval-live.js +30 -15
  209. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  210. package/src/lib/runtime/ai-eval.d.ts +9 -2
  211. package/src/lib/runtime/ai-eval.js +29 -2
  212. package/src/lib/runtime/ai-eval.js.map +1 -1
  213. package/src/lib/runtime/ai-gate.js +1 -1
  214. package/src/lib/runtime/ai-gate.js.map +1 -1
  215. package/src/lib/runtime/ai-node-tree.js +3 -3
  216. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  217. package/src/lib/runtime/ai-palette.d.ts +7 -1
  218. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  219. package/src/lib/runtime/ai-palette.generated.js +16 -13
  220. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  221. package/src/lib/runtime/ai-palette.js +7 -1
  222. package/src/lib/runtime/ai-palette.js.map +1 -1
  223. package/src/lib/runtime/seo-audit.d.ts +13 -63
  224. package/src/lib/runtime/seo-audit.js +22 -207
  225. package/src/lib/runtime/seo-audit.js.map +1 -1
  226. package/src/lib/runtime/site-inventory.js +29 -23
  227. package/src/lib/runtime/site-inventory.js.map +1 -1
  228. package/src/lib/server/ai-admin-org.js +1 -1
  229. package/src/lib/server/ai-admin-org.js.map +1 -1
  230. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  231. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  232. package/src/lib/server/ai-admin-signals.js +1 -1
  233. package/src/lib/server/ai-admin-signals.js.map +1 -1
  234. package/src/lib/server/ai-allotments.js +1 -1
  235. package/src/lib/server/ai-allotments.js.map +1 -1
  236. package/src/lib/server/ai-assist-prompts.js +3 -3
  237. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  238. package/src/lib/server/ai-assist.js +2 -2
  239. package/src/lib/server/ai-assist.js.map +1 -1
  240. package/src/lib/server/assist-chat.d.ts +1 -1
  241. package/src/lib/server/assist-chat.js +13 -12
  242. package/src/lib/server/assist-chat.js.map +1 -1
  243. package/src/lib/server/assist-view-context.js +23 -23
  244. package/src/lib/server/assist-view-context.js.map +1 -1
  245. package/src/lib/server/billing-credits.d.ts +5 -0
  246. package/src/lib/server/billing-credits.js +8 -2
  247. package/src/lib/server/billing-credits.js.map +1 -1
  248. package/src/lib/server/billing-overage.js +2 -2
  249. package/src/lib/server/billing-overage.js.map +1 -1
  250. package/src/lib/server.d.ts +1 -0
  251. package/src/lib/server.js.map +1 -1
  252. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  253. package/src/lib/testing/stand-in-automation-index.js +102 -0
  254. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  255. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  256. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  257. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  258. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  259. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  260. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  261. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  262. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  263. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  264. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  265. package/src/lib/testing/stand-in-product-index.js +105 -0
  266. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  267. package/src/lib/tools/ai-component-tool.js +2 -2
  268. package/src/lib/tools/ai-component-tool.js.map +1 -1
  269. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  270. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  271. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  272. package/src/lib/tools/ai-seo-tool.js +7 -38
  273. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  274. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  275. package/src/lib/tools/ai-workflow-tool.js +1 -1
  276. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  277. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  278. package/src/lib/usage/ai-allotment-alerts.js +39 -15
  279. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  280. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  281. package/src/lib/usage/ai-quota-checks.js +69 -0
  282. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  283. package/src/lib/usage/ai-usage-alerts.js +22 -10
  284. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  285. package/src/lib/usage/ai-usage-by-user.js +1 -1
  286. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  287. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  288. package/src/lib/usage/assist-ceiling.js +4 -4
  289. package/src/lib/usage/assist-ceiling.js.map +1 -1
  290. package/src/lib/usage/assist-credits.d.ts +494 -0
  291. package/src/lib/usage/assist-credits.js +524 -0
  292. package/src/lib/usage/assist-credits.js.map +1 -0
  293. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  294. package/src/lib/usage/assist-free-taste.js +3 -2
  295. package/src/lib/usage/assist-free-taste.js.map +1 -1
  296. package/src/lib/usage/assist-signal-mining.js +1 -1
  297. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  298. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  299. package/src/lib/usage/assist-spend-guards.js +182 -0
  300. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  301. package/src/lib/usage/assist-usage.d.ts +1 -1
  302. package/src/lib/usage/assist-usage.js +1 -1
  303. package/src/lib/usage/assist-usage.js.map +1 -1
  304. package/src/lib/usage/staff-org-ai.js +1 -1
  305. package/src/lib/usage/staff-org-ai.js.map +1 -1
  306. package/src/lib/usage-axes.d.ts +49 -0
  307. package/src/lib/usage-axes.js +108 -0
  308. package/src/lib/usage-axes.js.map +1 -0
  309. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  310. package/src/lib/runtime/seo-page-facts.js +0 -150
  311. package/src/lib/runtime/seo-page-facts.js.map +0 -1
package/README.md CHANGED
@@ -16,8 +16,8 @@ Peer dependencies: `react`, `next`, `@mui/material`, `@mui/x-data-grid`, `fireba
16
16
 
17
17
  **Console** (`registerAiConsole`, the `console` and `staff` registrar in `plugins.config.json`). The plugin adds no nav item. Everything is mounted through zones the console shell and other plugins host, so no console page imports it:
18
18
 
19
- - The assistant dock (`assistPanel`) and a provider the shell mounts around every console page for the Besigner copy assistant, with controls in the `besignerToolbar` and `besignerInspector` slots.
20
- - "Describe it" entry points and cards on the site's screens, components, layouts, templates, forms, theme, SEO and automations pages (`hostScreens`, `hostComponents`, `hostLayouts`, `hostTemplates`, `hostForms`, `hostTheme`, `hostSeo`, `seoFields`, `hostAutomations`, `automationEditor`, `automationRun`, `hostFirstRun`, `orgSites`).
19
+ - The assistant, drawn in the console dock (`consoleDock`) and a provider the shell mounts around every console page for the Besigner copy assistant, with controls in the `besignerToolbar` and `besignerInspector` slots.
20
+ - "Describe it" entry points and cards on a site's pages list and its components, layouts, templates, forms, theme, SEO and automations pages (`hostScreens`, `hostComponents`, `hostLayouts`, `hostTemplates`, `hostForms`, `hostTheme`, `hostSeo`, `seoFields`, `hostAutomations`, `automationEditor`, `automationRun`, `hostFirstRun`, `orgSites`).
21
21
  - Cards in zones other plugins host: product copy, import and the products hub (`productEditor`, `productImport`, `productsHub`), record insights, email drafts and import column matching (`recordInsights`, `recordEmail`, `importMapping`), and A/B test variants and results (`experimentVariants`, `experimentResult`).
22
22
  - Usage and access: credit and usage cards on billing (`orgBillingUsage`), per-member cards and columns (`orgMember`, `orgMembersListColumn`, `hostMembers`), and staff cards, columns and one staff page (`staffOrg`, `staffUser`, `staffOrgsListColumn`, `staffOrgUsageColumn`).
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.218",
3
+ "version": "1.0.0-beta.220",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.218",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.218",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.218",
31
- "@aglyn/shared-data-types": "1.0.0-beta.218",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.218",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.218",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.218",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.218",
36
- "@aglyn/shared-util-email": "1.0.0-beta.218",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.218",
38
- "@aglyn/shared-util-http": "1.0.0-beta.218",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.218",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.218",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.218",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.218",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.218",
28
+ "@aglyn/aglyn": "1.0.0-beta.220",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.220",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.220",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.220",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.220",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.220",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.220",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.220",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.220",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.220",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.220",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.220",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.220",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.220",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.220",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.220",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -48,7 +48,7 @@
48
48
  "@mui/x-data-grid": "^9.12.0",
49
49
  "firebase": "^12.18.0",
50
50
  "firebase-admin": "^14.3.0",
51
- "next": "16.3.3",
51
+ "next": "^16.3.3",
52
52
  "react": "^19.2.8"
53
53
  },
54
54
  "sideEffects": [
package/src/index.d.ts CHANGED
@@ -19,3 +19,4 @@ export * from './lib/plugin';
19
19
  export * from './lib/plugin-config';
20
20
  export * from './lib/providers/catalog';
21
21
  export { registerAiDeclarations, AI_PLUGIN_ENTITLEMENTS } from './lib/declarations';
22
+ export type { aiPlanEntitlements } from './lib/plan-entitlements';
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../libs/plugins/ai/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/constants'\nexport * from './lib/plugin'\nexport * from './lib/plugin-config'\nexport * from './lib/providers/catalog'\nexport { registerAiDeclarations, AI_PLUGIN_ENTITLEMENTS } from './lib/declarations'\n"],"names":["registerAiDeclarations","AI_PLUGIN_ENTITLEMENTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,qBAAiB;AAC/B,cAAc,kBAAc;AAC5B,cAAc,yBAAqB;AACnC,cAAc,6BAAyB;AACvC,SAASA,sBAAsB,EAAEC,sBAAsB,QAAQ,wBAAoB"}
1
+ {"version":3,"sources":["../../../../../libs/plugins/ai/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/constants'\nexport * from './lib/plugin'\nexport * from './lib/plugin-config'\nexport * from './lib/providers/catalog'\nexport { registerAiDeclarations, AI_PLUGIN_ENTITLEMENTS } from './lib/declarations'\n// Type-only (AGL-3080): the plugin's entitlement keys are declared by module\n// augmentation in this module, so every program that loads the package\n// type-checks them. The figures reach core through the manifest generator.\nexport type { aiPlanEntitlements } from './lib/plan-entitlements'\n"],"names":["registerAiDeclarations","AI_PLUGIN_ENTITLEMENTS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,qBAAiB;AAC/B,cAAc,kBAAc;AAC5B,cAAc,yBAAqB;AACnC,cAAc,6BAAyB;AACvC,SAASA,sBAAsB,EAAEC,sBAAsB,QAAQ,wBAAoB"}
@@ -191,7 +191,14 @@ export function aiOutputTargetType(resource) {
191
191
  key,
192
192
  label: AI_ACTIVITY_ACTION_LABELS[key],
193
193
  scope: AI_ACTIVITY_ACTION_SCOPES[key]
194
- }))
194
+ })),
195
+ // A generation job (AGL-2904): the org feed names the job, and each of
196
+ // its outputs names the resource it produced. Filed as `aiJob` since
197
+ // before plugin target types were namespaced, so the noun is declared
198
+ // here rather than read off the type (AGL-3080).
199
+ targetTypes: {
200
+ aiJob: 'AI generation'
201
+ }
195
202
  });
196
203
  }
197
204
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/activity/ai-activity-actions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The AI actions an activity log can carry (AGL-2929).\n *\n * Every other action in `orgs/{orgId}/activity` and `hosts/{hostId}/activity`\n * is a prose sentence written at the call site — \"Saved the screen\". The AI\n * rows are CODES instead, because they are read by more than a person: the\n * org feed's \"AI\" chip, the staff audit facet and the actor table all need\n * to recognize an AI row without parsing a sentence, and a sentence that\n * three writers phrase three ways is three rows that look unrelated.\n *\n * One list, so the writers in `ai-activity.ts`, the feed presenter and the\n * staff facet cannot disagree about what counts as AI. A code missing from\n * `AI_ACTIVITY_ACTION_LABELS` fails the type, not the reader.\n */\n\nimport type { AiJobOutputResource } from '../model/ai-jobs.types'\nimport {\n pluginStaffAuditActionGroup,\n pluginStaffAuditActionGroupLabel,\n registerPluginActivityActions,\n type PluginActivityScope,\n} from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\n\n/** The action code each AI writer stores. */\nexport const AI_ACTIVITY_ACTIONS = {\n jobCreated: 'ai.job.created',\n jobOutput: 'ai.job.output',\n jobCanceled: 'ai.job.canceled',\n jobNeedsInput: 'ai.job.needs_input',\n editApplied: 'ai.edit.applied',\n seoApplied: 'ai.seo.applied',\n assistSection: 'ai.assist.section',\n overageHardCap: 'ai.overage.hardCap',\n overageCap: 'ai.overage.cap',\n allotmentChanged: 'ai.allotment.changed',\n permissionChanged: 'ai.permission.changed',\n addonPurchased: 'ai.addon.purchased',\n addonRemoved: 'ai.addon.removed',\n} as const\n\nexport type AiActivityAction =\n (typeof AI_ACTIVITY_ACTIONS)[keyof typeof AI_ACTIVITY_ACTIONS]\n\n/** What a person reads for each code, in the feed and the actor table. */\nexport const AI_ACTIVITY_ACTION_LABELS: Record<AiActivityAction, string> = {\n 'ai.job.created': 'Started an AI generation',\n 'ai.job.output': 'AI generated',\n 'ai.job.canceled': 'Canceled an AI generation',\n 'ai.job.needs_input': 'AI generation paused for input',\n 'ai.edit.applied': 'Applied AI edits',\n 'ai.seo.applied': 'Applied AI SEO fixes as drafts',\n 'ai.assist.section': 'AI generated a section',\n 'ai.overage.hardCap': 'AI stop-at-band switch',\n 'ai.overage.cap': 'AI overage ceiling',\n 'ai.allotment.changed': 'AI allotment changed',\n 'ai.permission.changed': 'AI permission changed',\n 'ai.addon.purchased': 'Added the AI add-on',\n 'ai.addon.removed': 'Removed the AI add-on',\n}\n\n/** Every code, in catalog order — what a filter sends as `isAnyOf`. */\nexport const AI_ACTIVITY_ACTION_LIST: readonly AiActivityAction[] =\n Object.values(AI_ACTIVITY_ACTIONS)\n\n/** The feed's filter chip and the staff facet's group, by one name. */\nexport const AI_ACTIVITY_FILTER_LABEL = 'AI'\n\n/**\n * Why a generation stopped and asked for a person (AGL-2904): the plan's\n * band ran out, the org's own ceiling refused, the monthly message cap or\n * the job's own token budget did — or, on the Free taste (AGL-2925), one of\n * its own precautions: the account's credits, its daily request cap, a\n * pause after declined briefs, or the platform's day of free spend. Those\n * are the meter's `AssistRefusedBy` less its `null`, so a ceiling the meter\n * can name is one the feed can label. The last three are the job's own: its\n * plan is ready for review, or an answer broke a building rule on its re-ask\n * (AGL-2935), or the site is at an allowance its plan sets for what the job\n * writes (AGL-2909).\n */\nexport type AiJobNeedsInputReason =\n | 'band'\n | 'cap'\n | 'messages'\n | 'budget'\n | 'allotment'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | 'plan'\n | 'doctrine'\n | 'limit'\n\nexport const AI_JOB_NEEDS_INPUT_REASON_LABELS: Record<\n AiJobNeedsInputReason,\n string\n> = {\n plan: 'its plan is ready for review',\n doctrine: 'its answer broke a building rule twice',\n limit: 'the site reached an allowance of its plan',\n band: 'the included AI band is used up',\n cap: 'the overage ceiling was reached',\n messages: 'the monthly message cap was reached',\n budget: 'the job budget was reached',\n allotment: 'the creator’s or the site’s AI allotment is used up',\n account: 'the account’s free AI credits are used up',\n requests: 'the daily free request cap was reached',\n refusals: 'free generation is paused for the day after declined briefs',\n platform: 'free AI generation is paused for the day',\n}\n\n/**\n * The feed target types a generation job's output can be filed under: the\n * kinds both activity logs know, so the org row and its host copy share a\n * shape.\n */\nexport type AiOutputTargetType =\n | 'screen'\n | 'layout'\n | 'component'\n | 'template'\n | 'workflow'\n | 'content'\n | 'theme'\n\n/**\n * Where the feed files each resource kind a job can write (AGL-2904).\n *\n * A job names its output by the resource it wrote, and the activity logs\n * know a narrower set of targets. The map is total, so a resource kind\n * added to the job document must say where the feed shows it before the\n * machine can log it. `reusableComponent` is the feed's `component`; a\n * `text` output is copy, which the feed files as `content`; a `theme`\n * proposal is filed under the site's theme, which the host feed links to the\n * Theme section (AGL-2938); the kinds whose runners have not shipped are\n * content of the site too, and are filed there until a target of their own\n * exists.\n */\nconst AI_OUTPUT_TARGET_TYPES: Record<AiJobOutputResource, AiOutputTargetType> = {\n screen: 'screen',\n reusableComponent: 'component',\n layout: 'layout',\n template: 'template',\n form: 'content',\n emailScreen: 'content',\n campaign: 'content',\n product: 'content',\n experiment: 'content',\n workflow: 'workflow',\n // An SEO proposal (AGL-2910) is about the site's content, and is filed there.\n seo: 'content',\n text: 'content',\n theme: 'theme',\n // An insight answer (AGL-2915) is about the site's figures; the feed row\n // names it and never what it says.\n insight: 'content',\n // A CRM proposal (AGL-2917) writes nothing; the feed files it with content.\n crm: 'content',\n}\n\nexport function aiOutputTargetType(resource: AiJobOutputResource): AiOutputTargetType {\n return AI_OUTPUT_TARGET_TYPES[resource]\n}\n\n/** Whether a stored action is one of the AI codes above. */\nexport function isAiActivityAction(action: unknown): action is AiActivityAction {\n return (\n typeof action === 'string' &&\n (AI_ACTIVITY_ACTION_LIST as readonly string[]).includes(action)\n )\n}\n\n/** The readable label for an AI code; `undefined` for any other action. */\nexport function aiActivityActionLabel(action: unknown): string | undefined {\n return isAiActivityAction(action)\n ? AI_ACTIVITY_ACTION_LABELS[action]\n : undefined\n}\n\n/**\n * Staff audit rows that are ABOUT AI without carrying an `ai.` code: the\n * customer's overage controls already write `billing.assistOverage.*`, and\n * the platform's free-spend auto-pause writes `platform.aiFreeSpend.*`\n * (AGL-2925). The audit page's facet folds them under the same group as the\n * codes, because \"what did we do about AI this week\" is one question.\n */\nexport const AI_STAFF_AUDIT_ACTION_PREFIXES: readonly string[] = [\n 'ai.',\n 'billing.assistOverage.',\n 'platform.aiFreeSpend.',\n]\n\n/** The facet group an `adminAudit` action belongs to. */\nexport const STAFF_AUDIT_AI_GROUP = 'ai'\n\n/**\n * Which log each code is written to. The job and edit codes land in both\n * the org feed and the host's copy; the billing and permission codes are\n * org-level facts and land in the org feed alone.\n */\nconst AI_ACTIVITY_ACTION_SCOPES: Record<\n AiActivityAction,\n PluginActivityScope | readonly PluginActivityScope[]\n> = {\n 'ai.job.created': ['org', 'host'],\n 'ai.job.output': ['org', 'host'],\n 'ai.job.canceled': ['org', 'host'],\n 'ai.job.needs_input': ['org', 'host'],\n 'ai.edit.applied': ['org', 'host'],\n 'ai.seo.applied': ['org', 'host'],\n 'ai.assist.section': ['org', 'host'],\n 'ai.overage.hardCap': 'org',\n 'ai.overage.cap': 'org',\n 'ai.allotment.changed': 'org',\n 'ai.permission.changed': 'org',\n 'ai.addon.purchased': 'org',\n 'ai.addon.removed': 'org',\n}\n\n/**\n * The catalog, declared through the generic activity-action seam\n * (AGL-2940) so the feed's chip, the actor table's filter and the staff\n * facet read it from the registry beside every other plugin's codes.\n * `registerAiDeclarations` calls it, in both apps and in the browser.\n */\nexport function registerAiActivityActions(): void {\n registerPluginActivityActions({\n pluginId: 'ai',\n group: {\n id: STAFF_AUDIT_AI_GROUP,\n label: AI_ACTIVITY_FILTER_LABEL,\n staffAuditPrefixes: AI_STAFF_AUDIT_ACTION_PREFIXES.filter(\n (prefix) => prefix !== 'ai.',\n ),\n // The staff cards opening on an org (AGL-2930) and on one account\n // (AGL-2928). Neither writes these any more; the rows already in the\n // log still classify as reads rather than changes.\n staffAuditAccessActions: ['org.ai-viewed', 'user.ai-usage-viewed'],\n },\n actions: AI_ACTIVITY_ACTION_LIST.map((key) => ({\n key,\n label: AI_ACTIVITY_ACTION_LABELS[key],\n scope: AI_ACTIVITY_ACTION_SCOPES[key],\n })),\n })\n}\n\n/**\n * The group the staff audit facet files an action under: `ai` for every\n * AI row, otherwise the action's leading namespace (`billing`, `org`,\n * `plugins`). Roughly seventy distinct actions write to `adminAudit`, so\n * the facet offers namespaces rather than a seventy-entry menu. Answered\n * by the registry, so every plugin's group is filed the same way.\n */\nexport function staffAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/** How a facet group reads in the menu: `ai` → `AI`, `billing` → `billing`. */\nexport function staffAuditActionGroupLabel(group: string): string {\n return pluginStaffAuditActionGroupLabel(group)\n}\n"],"names":["pluginStaffAuditActionGroup","pluginStaffAuditActionGroupLabel","registerPluginActivityActions","AI_ACTIVITY_ACTIONS","jobCreated","jobOutput","jobCanceled","jobNeedsInput","editApplied","seoApplied","assistSection","overageHardCap","overageCap","allotmentChanged","permissionChanged","addonPurchased","addonRemoved","AI_ACTIVITY_ACTION_LABELS","AI_ACTIVITY_ACTION_LIST","Object","values","AI_ACTIVITY_FILTER_LABEL","AI_JOB_NEEDS_INPUT_REASON_LABELS","plan","doctrine","limit","band","cap","messages","budget","allotment","account","requests","refusals","platform","AI_OUTPUT_TARGET_TYPES","screen","reusableComponent","layout","template","form","emailScreen","campaign","product","experiment","workflow","seo","text","theme","insight","crm","aiOutputTargetType","resource","isAiActivityAction","action","includes","aiActivityActionLabel","undefined","AI_STAFF_AUDIT_ACTION_PREFIXES","STAFF_AUDIT_AI_GROUP","AI_ACTIVITY_ACTION_SCOPES","registerAiActivityActions","pluginId","group","id","label","staffAuditPrefixes","filter","prefix","staffAuditAccessActions","actions","map","key","scope","staffAuditActionGroup","staffAuditActionGroupLabel"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/activity/ai-activity-actions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The AI actions an activity log can carry (AGL-2929).\n *\n * Every other action in `orgs/{orgId}/activity` and `hosts/{hostId}/activity`\n * is a prose sentence written at the call site — \"Saved the screen\". The AI\n * rows are CODES instead, because they are read by more than a person: the\n * org feed's \"AI\" chip, the staff audit facet and the actor table all need\n * to recognize an AI row without parsing a sentence, and a sentence that\n * three writers phrase three ways is three rows that look unrelated.\n *\n * One list, so the writers in `ai-activity.ts`, the feed presenter and the\n * staff facet cannot disagree about what counts as AI. A code missing from\n * `AI_ACTIVITY_ACTION_LABELS` fails the type, not the reader.\n */\n\nimport type { AiJobOutputResource } from '../model/ai-jobs.types'\nimport {\n pluginStaffAuditActionGroup,\n pluginStaffAuditActionGroupLabel,\n registerPluginActivityActions,\n type PluginActivityScope,\n} from '@aglyn/aglyn/plugin-manager/plugin-activity-actions'\n\n/** The action code each AI writer stores. */\nexport const AI_ACTIVITY_ACTIONS = {\n jobCreated: 'ai.job.created',\n jobOutput: 'ai.job.output',\n jobCanceled: 'ai.job.canceled',\n jobNeedsInput: 'ai.job.needs_input',\n editApplied: 'ai.edit.applied',\n seoApplied: 'ai.seo.applied',\n assistSection: 'ai.assist.section',\n overageHardCap: 'ai.overage.hardCap',\n overageCap: 'ai.overage.cap',\n allotmentChanged: 'ai.allotment.changed',\n permissionChanged: 'ai.permission.changed',\n addonPurchased: 'ai.addon.purchased',\n addonRemoved: 'ai.addon.removed',\n} as const\n\nexport type AiActivityAction =\n (typeof AI_ACTIVITY_ACTIONS)[keyof typeof AI_ACTIVITY_ACTIONS]\n\n/** What a person reads for each code, in the feed and the actor table. */\nexport const AI_ACTIVITY_ACTION_LABELS: Record<AiActivityAction, string> = {\n 'ai.job.created': 'Started an AI generation',\n 'ai.job.output': 'AI generated',\n 'ai.job.canceled': 'Canceled an AI generation',\n 'ai.job.needs_input': 'AI generation paused for input',\n 'ai.edit.applied': 'Applied AI edits',\n 'ai.seo.applied': 'Applied AI SEO fixes as drafts',\n 'ai.assist.section': 'AI generated a section',\n 'ai.overage.hardCap': 'AI stop-at-band switch',\n 'ai.overage.cap': 'AI overage ceiling',\n 'ai.allotment.changed': 'AI allotment changed',\n 'ai.permission.changed': 'AI permission changed',\n 'ai.addon.purchased': 'Added the AI add-on',\n 'ai.addon.removed': 'Removed the AI add-on',\n}\n\n/** Every code, in catalog order — what a filter sends as `isAnyOf`. */\nexport const AI_ACTIVITY_ACTION_LIST: readonly AiActivityAction[] =\n Object.values(AI_ACTIVITY_ACTIONS)\n\n/** The feed's filter chip and the staff facet's group, by one name. */\nexport const AI_ACTIVITY_FILTER_LABEL = 'AI'\n\n/**\n * Why a generation stopped and asked for a person (AGL-2904): the plan's\n * band ran out, the org's own ceiling refused, the monthly message cap or\n * the job's own token budget did — or, on the Free taste (AGL-2925), one of\n * its own precautions: the account's credits, its daily request cap, a\n * pause after declined briefs, or the platform's day of free spend. Those\n * are the meter's `AssistRefusedBy` less its `null`, so a ceiling the meter\n * can name is one the feed can label. The last three are the job's own: its\n * plan is ready for review, or an answer broke a building rule on its re-ask\n * (AGL-2935), or the site is at an allowance its plan sets for what the job\n * writes (AGL-2909).\n */\nexport type AiJobNeedsInputReason =\n | 'band'\n | 'cap'\n | 'messages'\n | 'budget'\n | 'allotment'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | 'plan'\n | 'doctrine'\n | 'limit'\n\nexport const AI_JOB_NEEDS_INPUT_REASON_LABELS: Record<\n AiJobNeedsInputReason,\n string\n> = {\n plan: 'its plan is ready for review',\n doctrine: 'its answer broke a building rule twice',\n limit: 'the site reached an allowance of its plan',\n band: 'the included AI band is used up',\n cap: 'the overage ceiling was reached',\n messages: 'the monthly message cap was reached',\n budget: 'the job budget was reached',\n allotment: 'the creator’s or the site’s AI allotment is used up',\n account: 'the account’s free AI credits are used up',\n requests: 'the daily free request cap was reached',\n refusals: 'free generation is paused for the day after declined briefs',\n platform: 'free AI generation is paused for the day',\n}\n\n/**\n * The feed target types a generation job's output can be filed under: the\n * kinds both activity logs know, so the org row and its host copy share a\n * shape.\n */\nexport type AiOutputTargetType =\n | 'screen'\n | 'layout'\n | 'component'\n | 'template'\n | 'workflow'\n | 'content'\n | 'theme'\n\n/**\n * Where the feed files each resource kind a job can write (AGL-2904).\n *\n * A job names its output by the resource it wrote, and the activity logs\n * know a narrower set of targets. The map is total, so a resource kind\n * added to the job document must say where the feed shows it before the\n * machine can log it. `reusableComponent` is the feed's `component`; a\n * `text` output is copy, which the feed files as `content`; a `theme`\n * proposal is filed under the site's theme, which the host feed links to the\n * Theme section (AGL-2938); the kinds whose runners have not shipped are\n * content of the site too, and are filed there until a target of their own\n * exists.\n */\nconst AI_OUTPUT_TARGET_TYPES: Record<AiJobOutputResource, AiOutputTargetType> = {\n screen: 'screen',\n reusableComponent: 'component',\n layout: 'layout',\n template: 'template',\n form: 'content',\n emailScreen: 'content',\n campaign: 'content',\n product: 'content',\n experiment: 'content',\n workflow: 'workflow',\n // An SEO proposal (AGL-2910) is about the site's content, and is filed there.\n seo: 'content',\n text: 'content',\n theme: 'theme',\n // An insight answer (AGL-2915) is about the site's figures; the feed row\n // names it and never what it says.\n insight: 'content',\n // A CRM proposal (AGL-2917) writes nothing; the feed files it with content.\n crm: 'content',\n}\n\nexport function aiOutputTargetType(resource: AiJobOutputResource): AiOutputTargetType {\n return AI_OUTPUT_TARGET_TYPES[resource]\n}\n\n/** Whether a stored action is one of the AI codes above. */\nexport function isAiActivityAction(action: unknown): action is AiActivityAction {\n return (\n typeof action === 'string' &&\n (AI_ACTIVITY_ACTION_LIST as readonly string[]).includes(action)\n )\n}\n\n/** The readable label for an AI code; `undefined` for any other action. */\nexport function aiActivityActionLabel(action: unknown): string | undefined {\n return isAiActivityAction(action)\n ? AI_ACTIVITY_ACTION_LABELS[action]\n : undefined\n}\n\n/**\n * Staff audit rows that are ABOUT AI without carrying an `ai.` code: the\n * customer's overage controls already write `billing.assistOverage.*`, and\n * the platform's free-spend auto-pause writes `platform.aiFreeSpend.*`\n * (AGL-2925). The audit page's facet folds them under the same group as the\n * codes, because \"what did we do about AI this week\" is one question.\n */\nexport const AI_STAFF_AUDIT_ACTION_PREFIXES: readonly string[] = [\n 'ai.',\n 'billing.assistOverage.',\n 'platform.aiFreeSpend.',\n]\n\n/** The facet group an `adminAudit` action belongs to. */\nexport const STAFF_AUDIT_AI_GROUP = 'ai'\n\n/**\n * Which log each code is written to. The job and edit codes land in both\n * the org feed and the host's copy; the billing and permission codes are\n * org-level facts and land in the org feed alone.\n */\nconst AI_ACTIVITY_ACTION_SCOPES: Record<\n AiActivityAction,\n PluginActivityScope | readonly PluginActivityScope[]\n> = {\n 'ai.job.created': ['org', 'host'],\n 'ai.job.output': ['org', 'host'],\n 'ai.job.canceled': ['org', 'host'],\n 'ai.job.needs_input': ['org', 'host'],\n 'ai.edit.applied': ['org', 'host'],\n 'ai.seo.applied': ['org', 'host'],\n 'ai.assist.section': ['org', 'host'],\n 'ai.overage.hardCap': 'org',\n 'ai.overage.cap': 'org',\n 'ai.allotment.changed': 'org',\n 'ai.permission.changed': 'org',\n 'ai.addon.purchased': 'org',\n 'ai.addon.removed': 'org',\n}\n\n/**\n * The catalog, declared through the generic activity-action seam\n * (AGL-2940) so the feed's chip, the actor table's filter and the staff\n * facet read it from the registry beside every other plugin's codes.\n * `registerAiDeclarations` calls it, in both apps and in the browser.\n */\nexport function registerAiActivityActions(): void {\n registerPluginActivityActions({\n pluginId: 'ai',\n group: {\n id: STAFF_AUDIT_AI_GROUP,\n label: AI_ACTIVITY_FILTER_LABEL,\n staffAuditPrefixes: AI_STAFF_AUDIT_ACTION_PREFIXES.filter(\n (prefix) => prefix !== 'ai.',\n ),\n // The staff cards opening on an org (AGL-2930) and on one account\n // (AGL-2928). Neither writes these any more; the rows already in the\n // log still classify as reads rather than changes.\n staffAuditAccessActions: ['org.ai-viewed', 'user.ai-usage-viewed'],\n },\n actions: AI_ACTIVITY_ACTION_LIST.map((key) => ({\n key,\n label: AI_ACTIVITY_ACTION_LABELS[key],\n scope: AI_ACTIVITY_ACTION_SCOPES[key],\n })),\n // A generation job (AGL-2904): the org feed names the job, and each of\n // its outputs names the resource it produced. Filed as `aiJob` since\n // before plugin target types were namespaced, so the noun is declared\n // here rather than read off the type (AGL-3080).\n targetTypes: { aiJob: 'AI generation' },\n })\n}\n\n/**\n * The group the staff audit facet files an action under: `ai` for every\n * AI row, otherwise the action's leading namespace (`billing`, `org`,\n * `plugins`). Roughly seventy distinct actions write to `adminAudit`, so\n * the facet offers namespaces rather than a seventy-entry menu. Answered\n * by the registry, so every plugin's group is filed the same way.\n */\nexport function staffAuditActionGroup(action: unknown): string {\n return pluginStaffAuditActionGroup(action)\n}\n\n/** How a facet group reads in the menu: `ai` → `AI`, `billing` → `billing`. */\nexport function staffAuditActionGroupLabel(group: string): string {\n return pluginStaffAuditActionGroupLabel(group)\n}\n"],"names":["pluginStaffAuditActionGroup","pluginStaffAuditActionGroupLabel","registerPluginActivityActions","AI_ACTIVITY_ACTIONS","jobCreated","jobOutput","jobCanceled","jobNeedsInput","editApplied","seoApplied","assistSection","overageHardCap","overageCap","allotmentChanged","permissionChanged","addonPurchased","addonRemoved","AI_ACTIVITY_ACTION_LABELS","AI_ACTIVITY_ACTION_LIST","Object","values","AI_ACTIVITY_FILTER_LABEL","AI_JOB_NEEDS_INPUT_REASON_LABELS","plan","doctrine","limit","band","cap","messages","budget","allotment","account","requests","refusals","platform","AI_OUTPUT_TARGET_TYPES","screen","reusableComponent","layout","template","form","emailScreen","campaign","product","experiment","workflow","seo","text","theme","insight","crm","aiOutputTargetType","resource","isAiActivityAction","action","includes","aiActivityActionLabel","undefined","AI_STAFF_AUDIT_ACTION_PREFIXES","STAFF_AUDIT_AI_GROUP","AI_ACTIVITY_ACTION_SCOPES","registerAiActivityActions","pluginId","group","id","label","staffAuditPrefixes","filter","prefix","staffAuditAccessActions","actions","map","key","scope","targetTypes","aiJob","staffAuditActionGroup","staffAuditActionGroupLabel"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginUsageMeterCloseContext, PluginUsageMeterContext, PluginUsageMeterReading } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters';
18
+ /**
19
+ * One workspace's month of Aglyn Assist, as the usage sweep records and bills
20
+ * it (AGL-2280, AGL-2653, AGL-3011).
21
+ *
22
+ * THE SPEND is recorded and never billed. `assistCostUsd` is the provider
23
+ * figure of the two the month's document keeps (AGL-3015) — what the tokens
24
+ * cost US, at the provider's rates — and enters the cost model at ×1 through
25
+ * this plugin's cost axis, because the discount guardrail's whole job is to
26
+ * compare revenue with what a workspace costs. On the invoice it would charge
27
+ * for what the subscription already bought.
28
+ *
29
+ * THE OVERAGE is billed. The credits past the plan's band were sold at the
30
+ * plan's per-1,000 rate, so they enter the month's billed figure beside the
31
+ * platform's plan-priced overages. One derivation — `assistMonthOverage` turns
32
+ * the BILLED spend (`estCostUsd`) into credits, subtracts the band the
33
+ * workspace resolves to and prices the rest — so the customer's meter and this
34
+ * line cannot disagree about what was over. The workspace's hard-cap switch is
35
+ * not read here: it lives at the gate, and reading it at sweep time would let a
36
+ * flip on the 1st erase a month's overage.
37
+ *
38
+ * From the month `AI_OVERAGE_INVOICED_FROM` names, the overage is billed by
39
+ * invoice as it accrues instead, and leaves the meter figure so one month's
40
+ * usage reaches exactly one invoice. It is still priced and recorded either
41
+ * way, so a month billed through the other channel never reads as a month
42
+ * inside its band.
43
+ */
44
+ export declare function measureAiMonth(context: PluginUsageMeterContext): Promise<PluginUsageMeterReading>;
45
+ /**
46
+ * The close-out of a month whose overage is billed by invoice: the remainder
47
+ * that never reached the charge threshold, reconciled and invoiced
48
+ * (`ai-overage-close.ts`). Before the cutover month there is nothing of this
49
+ * plugin's to settle, and the workspace's billing document is never read.
50
+ */
51
+ export declare function closeAiMonth(context: PluginUsageMeterCloseContext): Promise<void>;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { ASSIST_PROVIDER_COST_FIELD, assistMonthOverage, assistProviderCostUsd } from "../usage/assist-credits.js";
17
+ import { aiOverageBillsByInvoice } from "./ai-overage-cutover.js";
18
+ import { aiOverageFirestore } from "./ai-overage-trigger.js";
19
+ /**
20
+ * One workspace's month of Aglyn Assist, as the usage sweep records and bills
21
+ * it (AGL-2280, AGL-2653, AGL-3011).
22
+ *
23
+ * THE SPEND is recorded and never billed. `assistCostUsd` is the provider
24
+ * figure of the two the month's document keeps (AGL-3015) — what the tokens
25
+ * cost US, at the provider's rates — and enters the cost model at ×1 through
26
+ * this plugin's cost axis, because the discount guardrail's whole job is to
27
+ * compare revenue with what a workspace costs. On the invoice it would charge
28
+ * for what the subscription already bought.
29
+ *
30
+ * THE OVERAGE is billed. The credits past the plan's band were sold at the
31
+ * plan's per-1,000 rate, so they enter the month's billed figure beside the
32
+ * platform's plan-priced overages. One derivation — `assistMonthOverage` turns
33
+ * the BILLED spend (`estCostUsd`) into credits, subtracts the band the
34
+ * workspace resolves to and prices the rest — so the customer's meter and this
35
+ * line cannot disagree about what was over. The workspace's hard-cap switch is
36
+ * not read here: it lives at the gate, and reading it at sweep time would let a
37
+ * flip on the 1st erase a month's overage.
38
+ *
39
+ * From the month `AI_OVERAGE_INVOICED_FROM` names, the overage is billed by
40
+ * invoice as it accrues instead, and leaves the meter figure so one month's
41
+ * usage reaches exactly one invoice. It is still priced and recorded either
42
+ * way, so a month billed through the other channel never reads as a month
43
+ * inside its band.
44
+ */ export async function measureAiMonth(context) {
45
+ var _snapshot_get;
46
+ const snapshot = await aiOverageFirestore().collection('orgs').doc(context.orgId).collection('assistUsage').doc(context.month).get();
47
+ const assistCostUsd = assistProviderCostUsd(snapshot.get('estCostUsd'), snapshot.get(ASSIST_PROVIDER_COST_FIELD));
48
+ const billedRaw = Number((_snapshot_get = snapshot.get('estCostUsd')) != null ? _snapshot_get : 0);
49
+ const overage = assistMonthOverage(context.org, Number.isFinite(billedRaw) && billedRaw > 0 ? billedRaw : 0);
50
+ const billedByInvoice = aiOverageBillsByInvoice(context.month);
51
+ const meteredUsd = billedByInvoice ? 0 : overage.overageMonthlyUsd;
52
+ return {
53
+ fields: {
54
+ assistCostUsd,
55
+ // The credit view of the same month: what was drawn, the band it was
56
+ // drawn against (`null` where the plan sells none), the part past it
57
+ // and the rate it was priced at. Recorded always, zeros included, so a
58
+ // month inside its band is legible as one.
59
+ assistCredits: overage.usedCredits,
60
+ assistCreditsBand: overage.bandCredits,
61
+ assistCreditsOverage: overage.overageCredits,
62
+ assistOverageUsd: overage.overageMonthlyUsd,
63
+ assistOverageRateUsd: overage.overageRateUsd,
64
+ // What entered the meter figure, and whether the invoice channel took
65
+ // it instead — countable from the rows rather than inferred from a
66
+ // deployment variable.
67
+ assistOverageMeteredUsd: meteredUsd,
68
+ assistOverageBilledByPlugin: billedByInvoice
69
+ },
70
+ billedUsd: meteredUsd
71
+ };
72
+ }
73
+ /**
74
+ * The close-out of a month whose overage is billed by invoice: the remainder
75
+ * that never reached the charge threshold, reconciled and invoiced
76
+ * (`ai-overage-close.ts`). Before the cutover month there is nothing of this
77
+ * plugin's to settle, and the workspace's billing document is never read.
78
+ */ export async function closeAiMonth(context) {
79
+ if (!aiOverageBillsByInvoice(context.month)) return;
80
+ const { closeAiOverageMonth } = await import("./ai-overage-close.js");
81
+ await closeAiOverageMonth({
82
+ orgId: context.orgId,
83
+ month: context.month,
84
+ org: context.org,
85
+ stripeCustomerId: await context.stripeCustomerId()
86
+ });
87
+ }
88
+
89
+ //# sourceMappingURL=ai-month-meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-month-meter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistMonthOverage,\n assistProviderCostUsd,\n} from '../usage/assist-credits'\nimport type {\n PluginUsageMeterCloseContext,\n PluginUsageMeterContext,\n PluginUsageMeterReading,\n} from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { aiOverageBillsByInvoice } from './ai-overage-cutover'\nimport { aiOverageFirestore } from './ai-overage-trigger'\n\n/**\n * One workspace's month of Aglyn Assist, as the usage sweep records and bills\n * it (AGL-2280, AGL-2653, AGL-3011).\n *\n * THE SPEND is recorded and never billed. `assistCostUsd` is the provider\n * figure of the two the month's document keeps (AGL-3015) — what the tokens\n * cost US, at the provider's rates — and enters the cost model at ×1 through\n * this plugin's cost axis, because the discount guardrail's whole job is to\n * compare revenue with what a workspace costs. On the invoice it would charge\n * for what the subscription already bought.\n *\n * THE OVERAGE is billed. The credits past the plan's band were sold at the\n * plan's per-1,000 rate, so they enter the month's billed figure beside the\n * platform's plan-priced overages. One derivation — `assistMonthOverage` turns\n * the BILLED spend (`estCostUsd`) into credits, subtracts the band the\n * workspace resolves to and prices the rest — so the customer's meter and this\n * line cannot disagree about what was over. The workspace's hard-cap switch is\n * not read here: it lives at the gate, and reading it at sweep time would let a\n * flip on the 1st erase a month's overage.\n *\n * From the month `AI_OVERAGE_INVOICED_FROM` names, the overage is billed by\n * invoice as it accrues instead, and leaves the meter figure so one month's\n * usage reaches exactly one invoice. It is still priced and recorded either\n * way, so a month billed through the other channel never reads as a month\n * inside its band.\n */\nexport async function measureAiMonth(\n context: PluginUsageMeterContext,\n): Promise<PluginUsageMeterReading> {\n const snapshot = await aiOverageFirestore()\n .collection('orgs')\n .doc(context.orgId)\n .collection('assistUsage')\n .doc(context.month)\n .get()\n const assistCostUsd = assistProviderCostUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_PROVIDER_COST_FIELD),\n )\n const billedRaw = Number(snapshot.get('estCostUsd') ?? 0)\n const overage = assistMonthOverage(\n context.org as never,\n Number.isFinite(billedRaw) && billedRaw > 0 ? billedRaw : 0,\n )\n const billedByInvoice = aiOverageBillsByInvoice(context.month)\n const meteredUsd = billedByInvoice ? 0 : overage.overageMonthlyUsd\n return {\n fields: {\n assistCostUsd,\n // The credit view of the same month: what was drawn, the band it was\n // drawn against (`null` where the plan sells none), the part past it\n // and the rate it was priced at. Recorded always, zeros included, so a\n // month inside its band is legible as one.\n assistCredits: overage.usedCredits,\n assistCreditsBand: overage.bandCredits,\n assistCreditsOverage: overage.overageCredits,\n assistOverageUsd: overage.overageMonthlyUsd,\n assistOverageRateUsd: overage.overageRateUsd,\n // What entered the meter figure, and whether the invoice channel took\n // it instead — countable from the rows rather than inferred from a\n // deployment variable.\n assistOverageMeteredUsd: meteredUsd,\n assistOverageBilledByPlugin: billedByInvoice,\n },\n billedUsd: meteredUsd,\n }\n}\n\n/**\n * The close-out of a month whose overage is billed by invoice: the remainder\n * that never reached the charge threshold, reconciled and invoiced\n * (`ai-overage-close.ts`). Before the cutover month there is nothing of this\n * plugin's to settle, and the workspace's billing document is never read.\n */\nexport async function closeAiMonth(\n context: PluginUsageMeterCloseContext,\n): Promise<void> {\n if (!aiOverageBillsByInvoice(context.month)) return\n const { closeAiOverageMonth } = await import('./ai-overage-close')\n await closeAiOverageMonth({\n orgId: context.orgId,\n month: context.month,\n org: context.org,\n stripeCustomerId: await context.stripeCustomerId(),\n })\n}\n"],"names":["ASSIST_PROVIDER_COST_FIELD","assistMonthOverage","assistProviderCostUsd","aiOverageBillsByInvoice","aiOverageFirestore","measureAiMonth","context","snapshot","collection","doc","orgId","month","get","assistCostUsd","billedRaw","Number","overage","org","isFinite","billedByInvoice","meteredUsd","overageMonthlyUsd","fields","assistCredits","usedCredits","assistCreditsBand","bandCredits","assistCreditsOverage","overageCredits","assistOverageUsd","assistOverageRateUsd","overageRateUsd","assistOverageMeteredUsd","assistOverageBilledByPlugin","billedUsd","closeAiMonth","closeAiOverageMonth","stripeCustomerId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,EAC1BC,kBAAkB,EAClBC,qBAAqB,QAChB,6BAAyB;AAMhC,SAASC,uBAAuB,QAAQ,0BAAsB;AAC9D,SAASC,kBAAkB,QAAQ,0BAAsB;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeC,eACpBC,OAAgC;QAYPC;IAVzB,MAAMA,WAAW,MAAMH,qBACpBI,UAAU,CAAC,QACXC,GAAG,CAACH,QAAQI,KAAK,EACjBF,UAAU,CAAC,eACXC,GAAG,CAACH,QAAQK,KAAK,EACjBC,GAAG;IACN,MAAMC,gBAAgBX,sBACpBK,SAASK,GAAG,CAAC,eACbL,SAASK,GAAG,CAACZ;IAEf,MAAMc,YAAYC,QAAOR,gBAAAA,SAASK,GAAG,CAAC,yBAAbL,gBAA8B;IACvD,MAAMS,UAAUf,mBACdK,QAAQW,GAAG,EACXF,OAAOG,QAAQ,CAACJ,cAAcA,YAAY,IAAIA,YAAY;IAE5D,MAAMK,kBAAkBhB,wBAAwBG,QAAQK,KAAK;IAC7D,MAAMS,aAAaD,kBAAkB,IAAIH,QAAQK,iBAAiB;IAClE,OAAO;QACLC,QAAQ;YACNT;YACA,qEAAqE;YACrE,qEAAqE;YACrE,uEAAuE;YACvE,2CAA2C;YAC3CU,eAAeP,QAAQQ,WAAW;YAClCC,mBAAmBT,QAAQU,WAAW;YACtCC,sBAAsBX,QAAQY,cAAc;YAC5CC,kBAAkBb,QAAQK,iBAAiB;YAC3CS,sBAAsBd,QAAQe,cAAc;YAC5C,sEAAsE;YACtE,mEAAmE;YACnE,uBAAuB;YACvBC,yBAAyBZ;YACzBa,6BAA6Bd;QAC/B;QACAe,WAAWd;IACb;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAee,aACpB7B,OAAqC;IAErC,IAAI,CAACH,wBAAwBG,QAAQK,KAAK,GAAG;IAC7C,MAAM,EAAEyB,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC;IAC7C,MAAMA,oBAAoB;QACxB1B,OAAOJ,QAAQI,KAAK;QACpBC,OAAOL,QAAQK,KAAK;QACpBM,KAAKX,QAAQW,GAAG;QAChBoB,kBAAkB,MAAM/B,QAAQ+B,gBAAgB;IAClD;AACF"}
@@ -14,38 +14,16 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- /**
18
- * CLOSING A MONTH OF AI OVERAGE (AGL-3011).
19
- *
20
- * Charging as usage accrues always leaves a remainder: the part of a month
21
- * that never reached the threshold before the month ended. This settles it,
22
- * and does the two other things a month's end is the right moment for.
23
- *
24
- * Run from the platform's closed-month usage sweep through the metered-line
25
- * seam — the one job that already knows every workspace, its month and its
26
- * Stripe customer — rather than from a cron of its own. A second schedule
27
- * for the same fact is a second schedule to keep in step, and the sweep that
28
- * closes the month is by definition on time for this.
29
- *
30
- * ## Three things, in order
31
- *
32
- * 1. **Reconcile.** A claim that never reported an outcome is asked about in
33
- * Stripe, and either recorded or released. It comes first because a stuck
34
- * open charge blocks the close-out behind it.
35
- * 2. **Close out.** Whatever is left unbilled is invoiced, however small —
36
- * down to Stripe's minimum, below which it is carried rather than sent as
37
- * an invoice nobody can pay.
38
- * 3. **Correct the ladder.** A month that ended with an AI charge unpaid is
39
- * taken back off the workspace's qualifying months. A month in which a
40
- * workspace both paid an invoice and left an AI charge unpaid is not
41
- * evidence of the thing the ladder measures.
42
- *
43
- * Idempotent, because the sweep calls it every day the month is closed: the
44
- * reconcile is a no-op once the charge is settled, the close-out claims
45
- * nothing once there is nothing unbilled, and the month is dropped from the
46
- * ladder only while it is on it.
47
- */
48
- import type { PluginMeteredLineMonthContext } from '@aglyn/aglyn/plugin-manager/plugin-metered-lines';
17
+ /** One workspace's closed month, as the usage sweep's meter hands it over. */
18
+ export interface AiOverageMonthContext {
19
+ orgId: string;
20
+ /** `YYYY-MM`, the month that closed. */
21
+ month: string;
22
+ /** The workspace's own document, as the sweep already read it. */
23
+ org: Readonly<Record<string, unknown>>;
24
+ /** Its Stripe customer, or `null` when it has none. */
25
+ stripeCustomerId: string | null;
26
+ }
49
27
  /** What the close-out did, for the sweep's log and the specs. */
50
28
  export interface AiOverageCloseResult {
51
29
  reconciled: number;
@@ -59,4 +37,4 @@ export interface AiOverageCloseResult {
59
37
  * each step is attempted independently so one workspace's failed Stripe call
60
38
  * does not cost it the ladder correction it also needed.
61
39
  */
62
- export declare function closeAiOverageMonth(context: PluginMeteredLineMonthContext, now?: Date): Promise<AiOverageCloseResult>;
40
+ export declare function closeAiOverageMonth(context: AiOverageMonthContext, now?: Date): Promise<AiOverageCloseResult>;
@@ -20,9 +20,10 @@
20
20
  * that never reached the threshold before the month ended. This settles it,
21
21
  * and does the two other things a month's end is the right moment for.
22
22
  *
23
- * Run from the platform's closed-month usage sweep through the metered-line
24
- * seam — the one job that already knows every workspace, its month and its
25
- * Stripe customer — rather than from a cron of its own. A second schedule
23
+ * Run from the platform's closed-month usage sweep through this plugin's
24
+ * usage meter (`ai-month-meter.ts`) — the one job that already knows every
25
+ * workspace, its month and its Stripe customer — rather than from a cron of
26
+ * its own. A second schedule
26
27
  * for the same fact is a second schedule to keep in step, and the sweep that
27
28
  * closes the month is by definition on time for this.
28
29
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-close.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * CLOSING A MONTH OF AI OVERAGE (AGL-3011).\n *\n * Charging as usage accrues always leaves a remainder: the part of a month\n * that never reached the threshold before the month ended. This settles it,\n * and does the two other things a month's end is the right moment for.\n *\n * Run from the platform's closed-month usage sweep through the metered-line\n * seam — the one job that already knows every workspace, its month and its\n * Stripe customer — rather than from a cron of its own. A second schedule\n * for the same fact is a second schedule to keep in step, and the sweep that\n * closes the month is by definition on time for this.\n *\n * ## Three things, in order\n *\n * 1. **Reconcile.** A claim that never reported an outcome is asked about in\n * Stripe, and either recorded or released. It comes first because a stuck\n * open charge blocks the close-out behind it.\n * 2. **Close out.** Whatever is left unbilled is invoiced, however small —\n * down to Stripe's minimum, below which it is carried rather than sent as\n * an invoice nobody can pay.\n * 3. **Correct the ladder.** A month that ended with an AI charge unpaid is\n * taken back off the workspace's qualifying months. A month in which a\n * workspace both paid an invoice and left an AI charge unpaid is not\n * evidence of the thing the ladder measures.\n *\n * Idempotent, because the sweep calls it every day the month is closed: the\n * reconcile is a no-op once the charge is settled, the close-out claims\n * nothing once there is nothing unbilled, and the month is dropped from the\n * ladder only while it is on it.\n */\n\nimport type { PluginMeteredLineMonthContext } from '@aglyn/aglyn/plugin-manager/plugin-metered-lines'\nimport {\n closeOutAiOverage,\n reconcileAiOverageCharge,\n} from './ai-overage-charge'\nimport {\n AI_OVERAGE_CHARGES_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from './ai-overage-ledger'\nimport { dropAiOverageQualifyingMonth } from './ai-overage-standing-writes'\nimport { aiOverageFirestore } from './ai-overage-trigger'\n\n/** How many of a month's charge rows the close-out will look at. */\nconst CHARGE_SCAN = 100\n\n/** What the close-out did, for the sweep's log and the specs. */\nexport interface AiOverageCloseResult {\n reconciled: number\n closedOutUsd: number\n droppedFromLadder: boolean\n}\n\n/**\n * Settles one workspace's closed month.\n *\n * Errors are the caller's to isolate — the registry logs and continues — but\n * each step is attempted independently so one workspace's failed Stripe call\n * does not cost it the ladder correction it also needed.\n */\nexport async function closeAiOverageMonth(\n context: PluginMeteredLineMonthContext,\n now = new Date(),\n): Promise<AiOverageCloseResult> {\n const firestore = aiOverageFirestore()\n const orgRef = firestore.collection('orgs').doc(context.orgId)\n const usageSnapshot = await orgRef\n .collection('assistUsage')\n .doc(context.month)\n .get()\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const result: AiOverageCloseResult = {\n reconciled: 0,\n closedOutUsd: 0,\n droppedFromLadder: false,\n }\n // A month that never sold any overage has nothing here at all, and asking\n // Stripe about it would be a round trip per workspace per day.\n if (!ledger.charges && !ledger.invoicedUsd && !ledger.open) {\n return result\n }\n\n // 1. An open claim that never reported. `since` is on the open slot, so\n // its age is known without a second read.\n if (ledger.open) {\n const open = usageSnapshot.get('overageInvoiceOpen') as\n | { since?: unknown }\n | null\n | undefined\n const since = Date.parse(String(open?.since ?? ''))\n const claimedAgeMs = Number.isFinite(since)\n ? Math.max(0, now.getTime() - since)\n : Number.MAX_SAFE_INTEGER\n const reconciled = await reconcileAiOverageCharge(firestore, {\n orgId: context.orgId,\n month: context.month,\n chargeId: ledger.open.chargeId,\n claimedAgeMs,\n })\n if (reconciled.resolved) result.reconciled = 1\n }\n\n // 2. The remainder.\n const closed = await closeOutAiOverage(firestore, {\n orgId: context.orgId,\n org: context.org as never,\n month: context.month,\n stripeCustomerId: context.stripeCustomerId,\n now,\n })\n if (closed.charged) {\n const after = await orgRef.collection('assistUsage').doc(context.month).get()\n result.closedOutUsd =\n readAiOverageMonthLedger(after.data() ?? null).invoicedUsd - ledger.invoicedUsd\n }\n\n // 3. The ladder. Read AFTER the two steps above, so a charge they settled\n // is not counted as unpaid.\n if (await aiOverageMonthLeftUnpaid(orgRef, context.month)) {\n await dropAiOverageQualifyingMonth(firestore, context.orgId, context.month)\n result.droppedFromLadder = true\n }\n return result\n}\n\n/**\n * Did this month end with an AI overage charge still unpaid?\n *\n * `failed` and `open` both count; `void` does not — an invoice written off\n * is a decision that the money is not owed, not a debt the workspace left.\n * A `claimed` row that survived the reconcile above counts too: something\n * claimed the money and never billed it, which is exactly the state that\n * should not raise a ceiling.\n */\nasync function aiOverageMonthLeftUnpaid(\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<boolean> {\n const charges = await orgRef\n .collection(AI_OVERAGE_CHARGES_SUBCOLLECTION)\n .where('month', '==', month)\n .limit(CHARGE_SCAN)\n .get()\n return charges.docs.some((doc) =>\n ['claimed', 'open', 'failed'].includes(String(doc.get('status') ?? '')),\n )\n}\n"],"names":["closeOutAiOverage","reconcileAiOverageCharge","AI_OVERAGE_CHARGES_SUBCOLLECTION","readAiOverageMonthLedger","dropAiOverageQualifyingMonth","aiOverageFirestore","CHARGE_SCAN","closeAiOverageMonth","context","now","Date","usageSnapshot","firestore","orgRef","collection","doc","orgId","month","get","ledger","exists","data","result","reconciled","closedOutUsd","droppedFromLadder","charges","invoicedUsd","open","since","parse","String","claimedAgeMs","Number","isFinite","Math","max","getTime","MAX_SAFE_INTEGER","chargeId","resolved","closed","org","stripeCustomerId","charged","after","aiOverageMonthLeftUnpaid","where","limit","docs","some","includes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-close.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * CLOSING A MONTH OF AI OVERAGE (AGL-3011).\n *\n * Charging as usage accrues always leaves a remainder: the part of a month\n * that never reached the threshold before the month ended. This settles it,\n * and does the two other things a month's end is the right moment for.\n *\n * Run from the platform's closed-month usage sweep through this plugin's\n * usage meter (`ai-month-meter.ts`) — the one job that already knows every\n * workspace, its month and its Stripe customer — rather than from a cron of\n * its own. A second schedule\n * for the same fact is a second schedule to keep in step, and the sweep that\n * closes the month is by definition on time for this.\n *\n * ## Three things, in order\n *\n * 1. **Reconcile.** A claim that never reported an outcome is asked about in\n * Stripe, and either recorded or released. It comes first because a stuck\n * open charge blocks the close-out behind it.\n * 2. **Close out.** Whatever is left unbilled is invoiced, however small —\n * down to Stripe's minimum, below which it is carried rather than sent as\n * an invoice nobody can pay.\n * 3. **Correct the ladder.** A month that ended with an AI charge unpaid is\n * taken back off the workspace's qualifying months. A month in which a\n * workspace both paid an invoice and left an AI charge unpaid is not\n * evidence of the thing the ladder measures.\n *\n * Idempotent, because the sweep calls it every day the month is closed: the\n * reconcile is a no-op once the charge is settled, the close-out claims\n * nothing once there is nothing unbilled, and the month is dropped from the\n * ladder only while it is on it.\n */\n\nimport {\n closeOutAiOverage,\n reconcileAiOverageCharge,\n} from './ai-overage-charge'\nimport {\n AI_OVERAGE_CHARGES_SUBCOLLECTION,\n readAiOverageMonthLedger,\n} from './ai-overage-ledger'\nimport { dropAiOverageQualifyingMonth } from './ai-overage-standing-writes'\nimport { aiOverageFirestore } from './ai-overage-trigger'\n\n/** One workspace's closed month, as the usage sweep's meter hands it over. */\nexport interface AiOverageMonthContext {\n orgId: string\n /** `YYYY-MM`, the month that closed. */\n month: string\n /** The workspace's own document, as the sweep already read it. */\n org: Readonly<Record<string, unknown>>\n /** Its Stripe customer, or `null` when it has none. */\n stripeCustomerId: string | null\n}\n\n/** How many of a month's charge rows the close-out will look at. */\nconst CHARGE_SCAN = 100\n\n/** What the close-out did, for the sweep's log and the specs. */\nexport interface AiOverageCloseResult {\n reconciled: number\n closedOutUsd: number\n droppedFromLadder: boolean\n}\n\n/**\n * Settles one workspace's closed month.\n *\n * Errors are the caller's to isolate — the registry logs and continues — but\n * each step is attempted independently so one workspace's failed Stripe call\n * does not cost it the ladder correction it also needed.\n */\nexport async function closeAiOverageMonth(\n context: AiOverageMonthContext,\n now = new Date(),\n): Promise<AiOverageCloseResult> {\n const firestore = aiOverageFirestore()\n const orgRef = firestore.collection('orgs').doc(context.orgId)\n const usageSnapshot = await orgRef\n .collection('assistUsage')\n .doc(context.month)\n .get()\n const ledger = readAiOverageMonthLedger(\n usageSnapshot.exists ? (usageSnapshot.data() ?? null) : null,\n )\n const result: AiOverageCloseResult = {\n reconciled: 0,\n closedOutUsd: 0,\n droppedFromLadder: false,\n }\n // A month that never sold any overage has nothing here at all, and asking\n // Stripe about it would be a round trip per workspace per day.\n if (!ledger.charges && !ledger.invoicedUsd && !ledger.open) {\n return result\n }\n\n // 1. An open claim that never reported. `since` is on the open slot, so\n // its age is known without a second read.\n if (ledger.open) {\n const open = usageSnapshot.get('overageInvoiceOpen') as\n | { since?: unknown }\n | null\n | undefined\n const since = Date.parse(String(open?.since ?? ''))\n const claimedAgeMs = Number.isFinite(since)\n ? Math.max(0, now.getTime() - since)\n : Number.MAX_SAFE_INTEGER\n const reconciled = await reconcileAiOverageCharge(firestore, {\n orgId: context.orgId,\n month: context.month,\n chargeId: ledger.open.chargeId,\n claimedAgeMs,\n })\n if (reconciled.resolved) result.reconciled = 1\n }\n\n // 2. The remainder.\n const closed = await closeOutAiOverage(firestore, {\n orgId: context.orgId,\n org: context.org as never,\n month: context.month,\n stripeCustomerId: context.stripeCustomerId,\n now,\n })\n if (closed.charged) {\n const after = await orgRef.collection('assistUsage').doc(context.month).get()\n result.closedOutUsd =\n readAiOverageMonthLedger(after.data() ?? null).invoicedUsd - ledger.invoicedUsd\n }\n\n // 3. The ladder. Read AFTER the two steps above, so a charge they settled\n // is not counted as unpaid.\n if (await aiOverageMonthLeftUnpaid(orgRef, context.month)) {\n await dropAiOverageQualifyingMonth(firestore, context.orgId, context.month)\n result.droppedFromLadder = true\n }\n return result\n}\n\n/**\n * Did this month end with an AI overage charge still unpaid?\n *\n * `failed` and `open` both count; `void` does not — an invoice written off\n * is a decision that the money is not owed, not a debt the workspace left.\n * A `claimed` row that survived the reconcile above counts too: something\n * claimed the money and never billed it, which is exactly the state that\n * should not raise a ceiling.\n */\nasync function aiOverageMonthLeftUnpaid(\n orgRef: FirebaseFirestore.DocumentReference,\n month: string,\n): Promise<boolean> {\n const charges = await orgRef\n .collection(AI_OVERAGE_CHARGES_SUBCOLLECTION)\n .where('month', '==', month)\n .limit(CHARGE_SCAN)\n .get()\n return charges.docs.some((doc) =>\n ['claimed', 'open', 'failed'].includes(String(doc.get('status') ?? '')),\n )\n}\n"],"names":["closeOutAiOverage","reconcileAiOverageCharge","AI_OVERAGE_CHARGES_SUBCOLLECTION","readAiOverageMonthLedger","dropAiOverageQualifyingMonth","aiOverageFirestore","CHARGE_SCAN","closeAiOverageMonth","context","now","Date","usageSnapshot","firestore","orgRef","collection","doc","orgId","month","get","ledger","exists","data","result","reconciled","closedOutUsd","droppedFromLadder","charges","invoicedUsd","open","since","parse","String","claimedAgeMs","Number","isFinite","Math","max","getTime","MAX_SAFE_INTEGER","chargeId","resolved","closed","org","stripeCustomerId","charged","after","aiOverageMonthLeftUnpaid","where","limit","docs","some","includes"],"mappings":"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"}
@@ -15,10 +15,35 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * Core's own name for the AI overage line in the monthly usage sweep. The
19
- * sweep asks the registry by this id; the plugin answers.
18
+ * THE CUTOVER (AGL-3011): one switch, and everything hangs off it.
19
+ *
20
+ * `AI_OVERAGE_INVOICED_FROM` names the first month (`YYYY-MM`) in which AI
21
+ * overage is billed by invoice as it accrues. Unset — which is how it ships
22
+ * — every workspace behaves exactly as it does today: overage accrues, the
23
+ * monthly sweep prices it into the meter event, and the renewal invoice
24
+ * bills it. Nothing charges mid-period and nothing new refuses.
25
+ *
26
+ * ## Why the GUARDS are behind the same switch as the charging
27
+ *
28
+ * The unpaid bound refuses a workspace whose accrued overage has not been
29
+ * paid for. Before cutover nothing charges, so nothing is ever paid for, so
30
+ * every workspace past $50 of overage would be refused with no invoice to
31
+ * pay and no way to clear it. A bound with no way to settle is not a bound,
32
+ * it is a wall — and a wall nobody agreed to.
33
+ *
34
+ * So the guards read this same switch. Turning it on turns on the charging
35
+ * and the guards together, which is the only combination in which each one
36
+ * makes sense.
37
+ *
38
+ * ## Why it is a month and not a boolean
39
+ *
40
+ * The meter and the invoice must never both bill one month's overage. A
41
+ * month is the unit the meter reports in, so a month is the unit the handover
42
+ * has to be expressed in: everything before the named month bills through the
43
+ * meter exactly as it always did, and everything from it bills by invoice.
44
+ * A boolean flipped mid-month would split one month's overage across both
45
+ * channels and bill part of it twice.
20
46
  */
21
- export declare const AI_OVERAGE_METER_LINE_ID = "assist-overage";
22
47
  /** The environment variable that starts mid-period charging. */
23
48
  export declare const AI_OVERAGE_INVOICED_FROM_ENV = "AI_OVERAGE_INVOICED_FROM";
24
49
  /**
@@ -52,12 +77,3 @@ export declare function aiOverageBillsByInvoice(month: string): boolean;
52
77
  * supposed to pay it.
53
78
  */
54
79
  export declare function aiOverageGuardsApply(month: string): boolean;
55
- /**
56
- * Claims the meter line, so the monthly sweep leaves AI overage out of the
57
- * figure it meters from the cutover month on.
58
- *
59
- * Registered from the plugin's server declarations, at boot, because the
60
- * sweep is a core cron that never loads an AI door. Idempotent by the
61
- * registry's own rule: the same plugin re-claiming replaces its claim.
62
- */
63
- export declare function registerAiOverageMeteredLine(): void;
@@ -42,13 +42,7 @@
42
42
  * meter exactly as it always did, and everything from it bills by invoice.
43
43
  * A boolean flipped mid-month would split one month's overage across both
44
44
  * channels and bill part of it twice.
45
- */ import { registerPluginMeteredLine } from "@aglyn/aglyn/plugin-manager/plugin-metered-lines";
46
- import { AI_PLUGIN_ID } from "../constants.js";
47
- /**
48
- * Core's own name for the AI overage line in the monthly usage sweep. The
49
- * sweep asks the registry by this id; the plugin answers.
50
- */ export const AI_OVERAGE_METER_LINE_ID = 'assist-overage';
51
- /** The environment variable that starts mid-period charging. */ export const AI_OVERAGE_INVOICED_FROM_ENV = 'AI_OVERAGE_INVOICED_FROM';
45
+ */ /** The environment variable that starts mid-period charging. */ export const AI_OVERAGE_INVOICED_FROM_ENV = 'AI_OVERAGE_INVOICED_FROM';
52
46
  const MONTH = /^\d{4}-\d{2}$/;
53
47
  /**
54
48
  * The configured cutover month, or `null` when there is none.
@@ -89,26 +83,5 @@ const MONTH = /^\d{4}-\d{2}$/;
89
83
  */ export function aiOverageGuardsApply(month) {
90
84
  return aiOverageBillsByInvoice(month);
91
85
  }
92
- /**
93
- * Claims the meter line, so the monthly sweep leaves AI overage out of the
94
- * figure it meters from the cutover month on.
95
- *
96
- * Registered from the plugin's server declarations, at boot, because the
97
- * sweep is a core cron that never loads an AI door. Idempotent by the
98
- * registry's own rule: the same plugin re-claiming replaces its claim.
99
- */ export function registerAiOverageMeteredLine() {
100
- registerPluginMeteredLine({
101
- lineId: AI_OVERAGE_METER_LINE_ID,
102
- pluginId: AI_PLUGIN_ID,
103
- billsFrom: aiOverageInvoicedFrom,
104
- // Loaded on the first closed month rather than at boot, so the console's
105
- // every process does not carry the Stripe path for a sweep that runs
106
- // once a day in one of them.
107
- closeMonth: async (context)=>{
108
- const { closeAiOverageMonth } = await import("./ai-overage-close.js");
109
- await closeAiOverageMonth(context);
110
- }
111
- });
112
- }
113
86
 
114
87
  //# sourceMappingURL=ai-overage-cutover.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-cutover.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CUTOVER (AGL-3011): one switch, and everything hangs off it.\n *\n * `AI_OVERAGE_INVOICED_FROM` names the first month (`YYYY-MM`) in which AI\n * overage is billed by invoice as it accrues. Unset — which is how it ships\n * — every workspace behaves exactly as it does today: overage accrues, the\n * monthly sweep prices it into the meter event, and the renewal invoice\n * bills it. Nothing charges mid-period and nothing new refuses.\n *\n * ## Why the GUARDS are behind the same switch as the charging\n *\n * The unpaid bound refuses a workspace whose accrued overage has not been\n * paid for. Before cutover nothing charges, so nothing is ever paid for, so\n * every workspace past $50 of overage would be refused with no invoice to\n * pay and no way to clear it. A bound with no way to settle is not a bound,\n * it is a wall — and a wall nobody agreed to.\n *\n * So the guards read this same switch. Turning it on turns on the charging\n * and the guards together, which is the only combination in which each one\n * makes sense.\n *\n * ## Why it is a month and not a boolean\n *\n * The meter and the invoice must never both bill one month's overage. A\n * month is the unit the meter reports in, so a month is the unit the handover\n * has to be expressed in: everything before the named month bills through the\n * meter exactly as it always did, and everything from it bills by invoice.\n * A boolean flipped mid-month would split one month's overage across both\n * channels and bill part of it twice.\n */\n\nimport { registerPluginMeteredLine } from '@aglyn/aglyn/plugin-manager/plugin-metered-lines'\nimport { AI_PLUGIN_ID } from '../constants'\n\n/**\n * Core's own name for the AI overage line in the monthly usage sweep. The\n * sweep asks the registry by this id; the plugin answers.\n */\nexport const AI_OVERAGE_METER_LINE_ID = 'assist-overage'\n\n/** The environment variable that starts mid-period charging. */\nexport const AI_OVERAGE_INVOICED_FROM_ENV = 'AI_OVERAGE_INVOICED_FROM'\n\nconst MONTH = /^\\d{4}-\\d{2}$/\n\n/**\n * The configured cutover month, or `null` when there is none.\n *\n * Read on every call rather than cached, so setting the variable takes\n * effect without a restart and a process that started before it was set is\n * not stuck a month behind.\n *\n * An unparseable value is `null`, not an error: a typo must leave the\n * platform billing through the channel that works, not stop billing.\n */\nexport function aiOverageInvoicedFrom(): string | null {\n const raw = String(process.env[AI_OVERAGE_INVOICED_FROM_ENV] ?? '').trim()\n return MONTH.test(raw) ? raw : null\n}\n\n/**\n * Is AI overage billed by invoice — rather than through the monthly meter —\n * for this month?\n *\n * `YYYY-MM` is zero-padded and fixed width, so it orders identically as a\n * string and as a date; the format checks are what make that true rather\n * than lucky.\n */\nexport function aiOverageBillsByInvoice(month: string): boolean {\n const from = aiOverageInvoicedFrom()\n if (!from) return false\n if (!MONTH.test(month)) return false\n return month >= from\n}\n\n/**\n * Are the overage guards live for this month?\n *\n * The same answer as `aiOverageBillsByInvoice`, given its own name because\n * it is asked for a different reason and a reader of the gate should not\n * have to work out why the gate consults a billing question. If the two ever\n * need to differ, this is where that would be said — and it would need a\n * paragraph explaining how a workspace refused for unpaid overage is\n * supposed to pay it.\n */\nexport function aiOverageGuardsApply(month: string): boolean {\n return aiOverageBillsByInvoice(month)\n}\n\n/**\n * Claims the meter line, so the monthly sweep leaves AI overage out of the\n * figure it meters from the cutover month on.\n *\n * Registered from the plugin's server declarations, at boot, because the\n * sweep is a core cron that never loads an AI door. Idempotent by the\n * registry's own rule: the same plugin re-claiming replaces its claim.\n */\nexport function registerAiOverageMeteredLine(): void {\n registerPluginMeteredLine({\n lineId: AI_OVERAGE_METER_LINE_ID,\n pluginId: AI_PLUGIN_ID,\n billsFrom: aiOverageInvoicedFrom,\n // Loaded on the first closed month rather than at boot, so the console's\n // every process does not carry the Stripe path for a sweep that runs\n // once a day in one of them.\n closeMonth: async (context) => {\n const { closeAiOverageMonth } = await import('./ai-overage-close')\n await closeAiOverageMonth(context)\n },\n })\n}\n"],"names":["registerPluginMeteredLine","AI_PLUGIN_ID","AI_OVERAGE_METER_LINE_ID","AI_OVERAGE_INVOICED_FROM_ENV","MONTH","aiOverageInvoicedFrom","process","raw","String","env","trim","test","aiOverageBillsByInvoice","month","from","aiOverageGuardsApply","registerAiOverageMeteredLine","lineId","pluginId","billsFrom","closeMonth","context","closeAiOverageMonth"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,SAASA,yBAAyB,QAAQ,mDAAkD;AAC5F,SAASC,YAAY,QAAQ,kBAAc;AAE3C;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,iBAAgB;AAExD,8DAA8D,GAC9D,OAAO,MAAMC,+BAA+B,2BAA0B;AAEtE,MAAMC,QAAQ;AAEd;;;;;;;;;CASC,GACD,OAAO,SAASC;QACKC;IAAnB,MAAMC,MAAMC,QAAOF,4CAAAA,QAAQG,GAAG,CAACN,6BAA6B,YAAzCG,4CAA6C,IAAII,IAAI;IACxE,OAAON,MAAMO,IAAI,CAACJ,OAAOA,MAAM;AACjC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASK,wBAAwBC,KAAa;IACnD,MAAMC,OAAOT;IACb,IAAI,CAACS,MAAM,OAAO;IAClB,IAAI,CAACV,MAAMO,IAAI,CAACE,QAAQ,OAAO;IAC/B,OAAOA,SAASC;AAClB;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,qBAAqBF,KAAa;IAChD,OAAOD,wBAAwBC;AACjC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASG;IACdhB,0BAA0B;QACxBiB,QAAQf;QACRgB,UAAUjB;QACVkB,WAAWd;QACX,yEAAyE;QACzE,qEAAqE;QACrE,6BAA6B;QAC7Be,YAAY,OAAOC;YACjB,MAAM,EAAEC,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC;YAC7C,MAAMA,oBAAoBD;QAC5B;IACF;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-cutover.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CUTOVER (AGL-3011): one switch, and everything hangs off it.\n *\n * `AI_OVERAGE_INVOICED_FROM` names the first month (`YYYY-MM`) in which AI\n * overage is billed by invoice as it accrues. Unset — which is how it ships\n * — every workspace behaves exactly as it does today: overage accrues, the\n * monthly sweep prices it into the meter event, and the renewal invoice\n * bills it. Nothing charges mid-period and nothing new refuses.\n *\n * ## Why the GUARDS are behind the same switch as the charging\n *\n * The unpaid bound refuses a workspace whose accrued overage has not been\n * paid for. Before cutover nothing charges, so nothing is ever paid for, so\n * every workspace past $50 of overage would be refused with no invoice to\n * pay and no way to clear it. A bound with no way to settle is not a bound,\n * it is a wall — and a wall nobody agreed to.\n *\n * So the guards read this same switch. Turning it on turns on the charging\n * and the guards together, which is the only combination in which each one\n * makes sense.\n *\n * ## Why it is a month and not a boolean\n *\n * The meter and the invoice must never both bill one month's overage. A\n * month is the unit the meter reports in, so a month is the unit the handover\n * has to be expressed in: everything before the named month bills through the\n * meter exactly as it always did, and everything from it bills by invoice.\n * A boolean flipped mid-month would split one month's overage across both\n * channels and bill part of it twice.\n */\n\n/** The environment variable that starts mid-period charging. */\nexport const AI_OVERAGE_INVOICED_FROM_ENV = 'AI_OVERAGE_INVOICED_FROM'\n\nconst MONTH = /^\\d{4}-\\d{2}$/\n\n/**\n * The configured cutover month, or `null` when there is none.\n *\n * Read on every call rather than cached, so setting the variable takes\n * effect without a restart and a process that started before it was set is\n * not stuck a month behind.\n *\n * An unparseable value is `null`, not an error: a typo must leave the\n * platform billing through the channel that works, not stop billing.\n */\nexport function aiOverageInvoicedFrom(): string | null {\n const raw = String(process.env[AI_OVERAGE_INVOICED_FROM_ENV] ?? '').trim()\n return MONTH.test(raw) ? raw : null\n}\n\n/**\n * Is AI overage billed by invoice — rather than through the monthly meter —\n * for this month?\n *\n * `YYYY-MM` is zero-padded and fixed width, so it orders identically as a\n * string and as a date; the format checks are what make that true rather\n * than lucky.\n */\nexport function aiOverageBillsByInvoice(month: string): boolean {\n const from = aiOverageInvoicedFrom()\n if (!from) return false\n if (!MONTH.test(month)) return false\n return month >= from\n}\n\n/**\n * Are the overage guards live for this month?\n *\n * The same answer as `aiOverageBillsByInvoice`, given its own name because\n * it is asked for a different reason and a reader of the gate should not\n * have to work out why the gate consults a billing question. If the two ever\n * need to differ, this is where that would be said — and it would need a\n * paragraph explaining how a workspace refused for unpaid overage is\n * supposed to pay it.\n */\nexport function aiOverageGuardsApply(month: string): boolean {\n return aiOverageBillsByInvoice(month)\n}\n"],"names":["AI_OVERAGE_INVOICED_FROM_ENV","MONTH","aiOverageInvoicedFrom","process","raw","String","env","trim","test","aiOverageBillsByInvoice","month","from","aiOverageGuardsApply"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,8DAA8D,GAC9D,OAAO,MAAMA,+BAA+B,2BAA0B;AAEtE,MAAMC,QAAQ;AAEd;;;;;;;;;CASC,GACD,OAAO,SAASC;QACKC;IAAnB,MAAMC,MAAMC,QAAOF,4CAAAA,QAAQG,GAAG,CAACN,6BAA6B,YAAzCG,4CAA6C,IAAII,IAAI;IACxE,OAAON,MAAMO,IAAI,CAACJ,OAAOA,MAAM;AACjC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASK,wBAAwBC,KAAa;IACnD,MAAMC,OAAOT;IACb,IAAI,CAACS,MAAM,OAAO;IAClB,IAAI,CAACV,MAAMO,IAAI,CAACE,QAAQ,OAAO;IAC/B,OAAOA,SAASC;AAClB;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,qBAAqBF,KAAa;IAChD,OAAOD,wBAAwBC;AACjC"}
@@ -55,7 +55,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
55
55
  * Both are server-written: the rules allow the same readers as
56
56
  * `aiAllotments` and no writer at all.
57
57
  */ import { FieldValue } from "firebase-admin/firestore";
58
- import { assistMonthOverage } from "@aglyn/aglyn/app-utils/assist-credits";
58
+ import { assistMonthOverage } from "../usage/assist-credits.js";
59
59
  import { AI_OVERAGE_MIN_CHARGE_USD, AI_OVERAGE_THRESHOLD_USD, aiOverageCardOnFile, readAiOverageStanding } from "./ai-overage-standing.js";
60
60
  /** The subcollection and document the standing lives at. */ export const AI_BILLING_SUBCOLLECTION = 'aiBilling';
61
61
  export const AI_BILLING_STANDING_DOC = 'standing';