@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
@@ -29,9 +29,9 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
29
29
  * caller, so a caller establishes, before it asks for anything:
30
30
  *
31
31
  * 1. a site is named, and it is the job's own org's;
32
- * 2. the owning plugin runs on that site — switched on for the workspace,
33
- * not switched off for the site, and past its release flag — and has
34
- * actually registered its writer in this process;
32
+ * 2. the owning plugin — whichever registered the writer; this plugin names
33
+ * none — runs on that site, switched on for the workspace, not switched
34
+ * off for the site, and past its release flag;
35
35
  * 3. whatever the kind checks of its own, such as the plan a campaign needs;
36
36
  * 4. that the member the drafts are for may create one of each, which only
37
37
  * the owner can answer, and does, through `refusal`.
@@ -46,12 +46,18 @@ import type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admi
46
46
  export type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null;
47
47
  /** The registered writer for a resource, or `null` when no plugin writes it here. */
48
48
  export declare const aiPluginDraftWriter: AiPluginDraftWriterLookup;
49
- /** One draft a job kind needs written, and who owns it. */
49
+ /**
50
+ * The plugin that registered the writer for a resource, or `null` when none
51
+ * did in this process. The owner is whoever writes the resource — the
52
+ * registry knows it, so this plugin names none (AGL-3080).
53
+ */
54
+ export type AiPluginDraftOwnerLookup = (resource: string) => string | null;
55
+ /** The registered writer's plugin, from the core's registry. */
56
+ export declare const aiPluginDraftOwner: AiPluginDraftOwnerLookup;
57
+ /** One draft a job kind needs written. */
50
58
  export interface AiPluginDraftNeed {
51
59
  /** The resource name the owner registered its writer under. */
52
60
  resource: string;
53
- /** The owning plugin, as `plugins.config.json` names it. */
54
- pluginId: string;
55
61
  /** What the site's plugin list calls it, for the sentence a person reads. */
56
62
  label: string;
57
63
  }
@@ -64,6 +70,8 @@ export interface AiPluginDraftAdmissionInput {
64
70
  ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>;
65
71
  /** How a writer is found; the registry otherwise. */
66
72
  writerFor?: AiPluginDraftWriterLookup;
73
+ /** Which plugin owns a resource's writer; the registry otherwise. */
74
+ ownerFor?: AiPluginDraftOwnerLookup;
67
75
  /** The instant the owner's allowance is read against; now otherwise. */
68
76
  now?: Date;
69
77
  }
@@ -22,6 +22,11 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
22
22
  var _pluginResourceDraftWriter;
23
23
  return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.writer) != null ? _ref : null;
24
24
  };
25
+ /** The registered writer's plugin, from the core's registry. */ export const aiPluginDraftOwner = (resource)=>{
26
+ var _ref;
27
+ var _pluginResourceDraftWriter;
28
+ return (_ref = (_pluginResourceDraftWriter = pluginResourceDraftWriter(resource)) == null ? void 0 : _pluginResourceDraftWriter.pluginId) != null ? _ref : null;
29
+ };
25
30
  /** What a person is told when the plugin that owns a draft does not run here. */ export function aiPluginDraftUnavailable(label) {
26
31
  return `Turn on ${label} for this site before starting the job.`;
27
32
  }
@@ -29,7 +34,7 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
29
34
  * Whether a job of this kind may start on this site for this member; `null`
30
35
  * admits it. Nothing here writes, and nothing here spends.
31
36
  */ export async function aiPluginDraftAdmissionRefusal(context, input) {
32
- var _input_writerFor, _data, _input_now;
37
+ var _input_writerFor, _input_ownerFor, _data, _input_now;
33
38
  const { hostId } = context;
34
39
  if (!hostId) {
35
40
  return {
@@ -43,18 +48,21 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
43
48
  error: 'Unknown site'
44
49
  };
45
50
  const writerFor = (_input_writerFor = input.writerFor) != null ? _input_writerFor : aiPluginDraftWriter;
51
+ const ownerFor = (_input_ownerFor = input.ownerFor) != null ? _input_ownerFor : aiPluginDraftOwner;
46
52
  const host = (_data = (await context.firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null;
47
53
  const org = context.org;
54
+ const owners = input.drafts.map((draft)=>ownerFor(draft.resource));
48
55
  // One release-flag read for every owner at once: the answer is per org, and
49
56
  // a plugin subtracted here is one the site cannot reach at all.
50
- const released = new Set(await filterEnabledPluginsByReleaseFlags(input.drafts.map((draft)=>draft.pluginId), {
57
+ const released = new Set(await filterEnabledPluginsByReleaseFlags(owners.filter((owner)=>Boolean(owner)), {
51
58
  orgId: context.orgId,
52
59
  authorization: null
53
60
  }));
54
61
  const writers = [];
55
- for (const draft of input.drafts){
62
+ for (const [index, draft] of input.drafts.entries()){
56
63
  const writer = writerFor(draft.resource);
57
- if (!writer || !released.has(draft.pluginId) || !isHostPluginEnabled(org, host, draft.pluginId)) {
64
+ const owner = owners[index];
65
+ if (!writer || !owner || !released.has(owner) || !isHostPluginEnabled(org, host, owner)) {
58
66
  return {
59
67
  status: 403,
60
68
  error: aiPluginDraftUnavailable(draft.label)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin runs on that site — switched on for the workspace,\n * not switched off for the site, and past its release flag — and has\n * actually registered its writer in this process;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/** One draft a job kind needs written, and who owns it. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** The owning plugin, as `plugins.config.json` names it. */\n pluginId: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n input.drafts.map((draft) => draft.pluginId),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const draft of input.drafts) {\n const writer = writerFor(draft.resource)\n if (\n !writer ||\n !released.has(draft.pluginId) ||\n !isHostPluginEnabled(org, host, draft.pluginId)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","host","firestore","collection","doc","get","data","org","released","Set","drafts","map","draft","pluginId","authorization","writers","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,8CAA6C;AACjF,SACEC,yBAAyB,QAEpB,qDAAoD;AAC3D,SAASC,kCAAkC,QAAQ,gDAA+C;AAClG,SAASC,mBAAmB,QAAQ,gDAA+C;AAiCnF,mFAAmF,GACnF,OAAO,MAAMC,sBAAiD,CAACC;;QAC7DJ;oBAAAA,6BAAAA,0BAA0BI,8BAA1BJ,2BAAqCK,MAAM,mBAAI;EAAI;AAyBrD,+EAA+E,GAC/E,OAAO,SAASC,yBAAyBC,KAAa;IACpD,OAAO,CAAC,QAAQ,EAAEA,MAAM,uCAAuC,CAAC;AAClE;AAEA;;;CAGC,GACD,OAAO,eAAeC,8BACpBC,OAA8B,EAC9BC,KAAkC;QAShBA,kBACL,OA+BDA;IAvCZ,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,IAAI,CAACE,QAAQ;QACX,OAAO;YAAEC,QAAQ;YAAKC,OAAO,CAAC,kBAAkB,EAAEH,MAAMI,IAAI,CAAC,+BAA+B,CAAC;QAAC;IAChG;IACA,MAAMC,QAAQ,MAAMb,oBAAoBS;IACxC,IAAI,CAACI,SAASA,UAAUN,QAAQO,KAAK,EAAE,OAAO;QAAEJ,QAAQ;QAAKC,OAAO;IAAe;IAEnF,MAAMI,aAAYP,mBAAAA,MAAMO,SAAS,YAAfP,mBAAmBP;IACrC,MAAMe,QAAO,QAAA,AAAC,CAAA,MAAMT,QAAQU,SAAS,CAACC,UAAU,CAAC,SAASC,GAAG,CAACV,QAAQW,GAAG,EAAC,EAAGC,IAAI,cAApE,QAA0E;IACvF,MAAMC,MAAMf,QAAQe,GAAG;IACvB,4EAA4E;IAC5E,gEAAgE;IAChE,MAAMC,WAAW,IAAIC,IACnB,MAAMzB,mCACJS,MAAMiB,MAAM,CAACC,GAAG,CAAC,CAACC,QAAUA,MAAMC,QAAQ,GAC1C;QAAEd,OAAOP,QAAQO,KAAK;QAAEe,eAAe;IAAK;IAGhD,MAAMC,UAAuC,EAAE;IAC/C,KAAK,MAAMH,SAASnB,MAAMiB,MAAM,CAAE;QAChC,MAAMtB,SAASY,UAAUY,MAAMzB,QAAQ;QACvC,IACE,CAACC,UACD,CAACoB,SAASQ,GAAG,CAACJ,MAAMC,QAAQ,KAC5B,CAAC/B,oBAAoByB,KAAKN,MAAMW,MAAMC,QAAQ,GAC9C;YACA,OAAO;gBAAElB,QAAQ;gBAAKC,OAAOP,yBAAyBuB,MAAMtB,KAAK;YAAE;QACrE;QACAyB,QAAQE,IAAI,CAAC7B;IACf;IAEA,MAAM8B,MAAM,OAAMzB,MAAM0B,QAAQ,oBAAd1B,MAAM0B,QAAQ,MAAd1B,OAAiBC;IACnC,IAAIwB,KAAK,OAAOA;IAEhB,0EAA0E;IAC1E,wEAAwE;IACxE,2DAA2D;IAC3D,MAAME,MAAM5B,QAAQ4B,GAAG;IACvB,IAAI,CAACA,KAAK,OAAO;IACjB,MAAMC,OAAM5B,aAAAA,MAAM4B,GAAG,YAAT5B,aAAa,IAAI6B;IAC7B,KAAK,MAAMlC,UAAU2B,QAAS;QAC5B,MAAMQ,UAAU,MAAMnC,OAAOmC,OAAO,CAAC;YAAExB,OAAOP,QAAQO,KAAK;YAAEL;YAAQ0B;YAAKb;YAAKc;QAAI;QACnF,sEAAsE;QACtE,iEAAiE;QACjE,IAAIE,SAAS,OAAO;YAAE5B,QAAQ4B,QAAQ5B,MAAM,KAAK,MAAM,MAAM4B,QAAQ5B,MAAM;YAAEC,OAAO2B,QAAQ3B,KAAK;QAAC;IACpG;IACA,OAAO;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-plugin-drafts.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 { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n pluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport type { AiJobAdmissionContext, AiJobAdmissionRefusal } from './ai-job-admission'\n\n/**\n * Admitting a job whose drafts ANOTHER plugin writes (AGL-2912).\n *\n * `aiDraftAdmissionRefusal` admits the kinds this plugin writes itself, where\n * the allowance is one it can read. An email job writes no document of its\n * own: the email plugin owns the email design and the marketing plugin owns\n * the campaign, and each registers a writer on the `plugin-resource-drafts`\n * seam. So the question this answers is different, and it is the caller's\n * half of that seam's contract — the seam carries no authorization of the\n * caller, so a caller establishes, before it asks for anything:\n *\n * 1. a site is named, and it is the job's own org's;\n * 2. the owning plugin — whichever registered the writer; this plugin names\n * none — runs on that site, switched on for the workspace, not switched\n * off for the site, and past its release flag;\n * 3. whatever the kind checks of its own, such as the plan a campaign needs;\n * 4. that the member the drafts are for may create one of each, which only\n * the owner can answer, and does, through `refusal`.\n *\n * Asked at the create door before the job exists and again at the resume door\n * before a confirmed plan runs, so a plugin switched off, or an allowance used\n * up, in between is caught before the job spends. A refusal is the owner's own\n * sentence wherever there is one, so a person reads what the console would\n * have told them.\n */\n\n/** How a resource's draft writer is found; the core's registry by default. */\nexport type AiPluginDraftWriterLookup = (resource: string) => PluginResourceDraftWriter | null\n\n/** The registered writer for a resource, or `null` when no plugin writes it here. */\nexport const aiPluginDraftWriter: AiPluginDraftWriterLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.writer ?? null\n\n/**\n * The plugin that registered the writer for a resource, or `null` when none\n * did in this process. The owner is whoever writes the resource — the\n * registry knows it, so this plugin names none (AGL-3080).\n */\nexport type AiPluginDraftOwnerLookup = (resource: string) => string | null\n\n/** The registered writer's plugin, from the core's registry. */\nexport const aiPluginDraftOwner: AiPluginDraftOwnerLookup = (resource) =>\n pluginResourceDraftWriter(resource)?.pluginId ?? null\n\n/** One draft a job kind needs written. */\nexport interface AiPluginDraftNeed {\n /** The resource name the owner registered its writer under. */\n resource: string\n /** What the site's plugin list calls it, for the sentence a person reads. */\n label: string\n}\n\nexport interface AiPluginDraftAdmissionInput {\n /** The job kind, named in the refusal that asks for a site. */\n kind: AiJobKind\n /** Every draft the kind writes, each checked in turn. */\n drafts: readonly AiPluginDraftNeed[]\n /** The kind's own check, asked once the site and its plugins are known good. */\n ownCheck?: (hostId: string) => Promise<AiJobAdmissionRefusal | null>\n /** How a writer is found; the registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n /** Which plugin owns a resource's writer; the registry otherwise. */\n ownerFor?: AiPluginDraftOwnerLookup\n /** The instant the owner's allowance is read against; now otherwise. */\n now?: Date\n}\n\n/** What a person is told when the plugin that owns a draft does not run here. */\nexport function aiPluginDraftUnavailable(label: string): string {\n return `Turn on ${label} for this site before starting the job.`\n}\n\n/**\n * Whether a job of this kind may start on this site for this member; `null`\n * admits it. Nothing here writes, and nothing here spends.\n */\nexport async function aiPluginDraftAdmissionRefusal(\n context: AiJobAdmissionContext,\n input: AiPluginDraftAdmissionInput,\n): Promise<AiJobAdmissionRefusal | null> {\n const { hostId } = context\n if (!hostId) {\n return { status: 400, error: `Open the site the ${input.kind} is for before starting the job` }\n }\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n\n const writerFor = input.writerFor ?? aiPluginDraftWriter\n const ownerFor = input.ownerFor ?? aiPluginDraftOwner\n const host = (await context.firestore.collection('hosts').doc(hostId).get()).data() ?? null\n const org = context.org as { enabledPlugins?: string[] } | null\n const owners = input.drafts.map((draft) => ownerFor(draft.resource))\n // One release-flag read for every owner at once: the answer is per org, and\n // a plugin subtracted here is one the site cannot reach at all.\n const released = new Set(\n await filterEnabledPluginsByReleaseFlags(\n owners.filter((owner): owner is string => Boolean(owner)),\n { orgId: context.orgId, authorization: null },\n ),\n )\n const writers: PluginResourceDraftWriter[] = []\n for (const [index, draft] of input.drafts.entries()) {\n const writer = writerFor(draft.resource)\n const owner = owners[index]\n if (\n !writer ||\n !owner ||\n !released.has(owner) ||\n !isHostPluginEnabled(org, host, owner)\n ) {\n return { status: 403, error: aiPluginDraftUnavailable(draft.label) }\n }\n writers.push(writer)\n }\n\n const own = await input.ownCheck?.(hostId)\n if (own) return own\n\n // Whether this member may create one of each, in the owner's own words. A\n // door that names no member — the machine re-checking a job it stored —\n // asks nothing here, because there is nobody to ask about.\n const uid = context.uid\n if (!uid) return null\n const now = input.now ?? new Date()\n for (const writer of writers) {\n const refusal = await writer.refusal({ orgId: context.orgId, hostId, uid, org, now })\n // The seam's 409 is \"one already exists\", which for an admission is a\n // room-to-create answer rather than a fault the door can act on.\n if (refusal) return { status: refusal.status === 409 ? 403 : refusal.status, error: refusal.error }\n }\n return null\n}\n"],"names":["isHostPluginEnabled","pluginResourceDraftWriter","filterEnabledPluginsByReleaseFlags","resolveOrgIdForHost","aiPluginDraftWriter","resource","writer","aiPluginDraftOwner","pluginId","aiPluginDraftUnavailable","label","aiPluginDraftAdmissionRefusal","context","input","hostId","status","error","kind","owner","orgId","writerFor","ownerFor","host","firestore","collection","doc","get","data","org","owners","drafts","map","draft","released","Set","filter","Boolean","authorization","writers","index","entries","has","push","own","ownCheck","uid","now","Date","refusal"],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
17
18
  import type { AiJob } from '../model/ai-jobs.types';
18
19
  import { type AiProductPhotoRead } from '../model/ai-products';
19
20
  import { type AiJobAdmission } from './ai-job-admission';
@@ -88,6 +89,12 @@ export interface AiJobProductsStepDeps {
88
89
  image?: AiProductImageSeams;
89
90
  /** Whether a model reads pictures; the provider's own descriptor otherwise. */
90
91
  readsImages?: (model: string) => boolean;
92
+ /**
93
+ * Where the site's categories are read; the `productCategory` index the
94
+ * plugin that keeps them publishes otherwise. The eval harness hands its
95
+ * own, over the categories a case names.
96
+ */
97
+ categoryIndex?: PluginRecordIndex | null;
91
98
  }
92
99
  /** The step, with its reads replaceable where a spec needs them. */
93
100
  export declare function createAiJobProductsStep(deps?: AiJobProductsStepDeps): AiJobStepRunner;
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
18
19
  import { isHostPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
19
20
  import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
20
21
  import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
@@ -76,7 +77,6 @@ export const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for t
76
77
  unreadable: 'The photo could not be read, so the copy is written from the product’s words.',
77
78
  model: 'The photo was not read: the model this job ran on does not read pictures.'
78
79
  };
79
- /** The commerce plugin, as `plugins.config.json` names it. */ const COMMERCE_PLUGIN_ID = 'commerce';
80
80
  /**
81
81
  * ASSUMED: how long a catalog or categories pass reads the site before it
82
82
  * asks: the host document and, for categories, the category names.
@@ -135,40 +135,54 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
135
135
  name: str(seo['title']) || str(host['displayName']) || subdomain || ''
136
136
  };
137
137
  }
138
- /** The site's categories by name, as many as a request lists. */ async function readCategories(firestore, hostId) {
139
- const snapshot = await firestore.collection('hosts').doc(hostId).collection('productCategories').select('name').limit(AI_PRODUCT_CATEGORY_LIST_MAX).get();
140
- return snapshot.docs.map((doc)=>({
141
- id: doc.id,
142
- name: str(doc.get('name'))
143
- })).filter((category)=>category.name).sort((a, b)=>a.name.localeCompare(b.name));
138
+ /**
139
+ * The site's categories by name, as many as a request lists — from the
140
+ * `productCategory` index the plugin that keeps them publishes (AGL-3080).
141
+ * None when no plugin keeps categories here.
142
+ */ async function readCategories(hostId, given) {
143
+ var _pluginRecordIndex;
144
+ const index = given != null ? given : (_pluginRecordIndex = pluginRecordIndex('productCategory')) == null ? void 0 : _pluginRecordIndex.index;
145
+ if (!index) return [];
146
+ const { records } = await index.list({
147
+ hostId,
148
+ limit: AI_PRODUCT_CATEGORY_LIST_MAX
149
+ });
150
+ return records.map((record)=>({
151
+ id: record.id,
152
+ name: record.name
153
+ })).sort((a, b)=>a.name.localeCompare(b.name));
144
154
  }
145
- /** A saved product's facts, as a bulk pass reads them; `null` when it is gone. */ async function readProductFacts(firestore, hostId, productId) {
146
- var _data_seo;
147
- const snapshot = await firestore.collection('hosts').doc(hostId).collection('products').doc(productId).get();
148
- const data = snapshot.exists ? snapshot.data() : null;
149
- if (!data || data['deletedAt']) return null;
150
- const name = str(data['name']);
151
- if (!name) return null;
152
- const type = data['type'];
153
- const seo = (_data_seo = data['seo']) != null ? _data_seo : {};
154
- const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : [];
155
+ /**
156
+ * A saved product's facts, as a bulk pass reads them; `null` when it is gone
157
+ * — read through the `product` index the plugin that keeps products publishes
158
+ * (AGL-3080), which answers live, named products only.
159
+ */ async function readProductFacts(hostId, productId) {
160
+ var _pluginRecordIndex;
161
+ const record = await ((_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.index.get({
162
+ hostId,
163
+ id: productId
164
+ }));
165
+ if (!record) return null;
166
+ const facts = record.facts;
167
+ const type = facts['type'];
168
+ const list = (value)=>Array.isArray(value) ? value : [];
155
169
  return {
156
- id: productId,
157
- name,
170
+ id: record.id,
171
+ name: record.name,
158
172
  type: AI_PRODUCT_TYPES.includes(type) ? type : 'physical',
159
- text: str(data['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),
160
- tags: (Array.isArray(data['tags']) ? data['tags'] : []).map(str).filter(Boolean),
161
- categoryIds: (Array.isArray(data['categoryIds']) ? data['categoryIds'] : []).map(str).filter(Boolean),
162
- options: (Array.isArray(data['options']) ? data['options'] : []).slice(0, AI_PRODUCT_OPTIONS_MAX).map((option)=>{
173
+ text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),
174
+ tags: list(facts['tags']).map(str).filter(Boolean),
175
+ categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),
176
+ options: list(facts['options']).slice(0, AI_PRODUCT_OPTIONS_MAX).map((option)=>{
163
177
  const record = option != null ? option : {};
164
178
  return {
165
179
  name: str(record['name']),
166
- values: (Array.isArray(record['values']) ? record['values'] : []).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)
180
+ values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)
167
181
  };
168
182
  }).filter((option)=>option.name),
169
- imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,
170
- seoTitle: str(seo['title']),
171
- seoDescription: str(seo['description'])
183
+ imageUrl: str(facts['imageUrl']) || null,
184
+ seoTitle: str(facts['seoTitle']),
185
+ seoDescription: str(facts['seoDescription'])
172
186
  };
173
187
  }
174
188
  function modelOf(context) {
@@ -188,7 +202,7 @@ function modelOf(context) {
188
202
  /** Ask for one product's copy, and build its proposal from the answer. */ async function writeProductCopy(context, model, site, product, deps) {
189
203
  var _ref;
190
204
  const { job, firestore, signal } = context;
191
- const categories = await readCategories(firestore, site.hostId);
205
+ const categories = await readCategories(site.hostId, deps.categoryIndex);
192
206
  let photo = 'none';
193
207
  let image = null;
194
208
  if (product.imageUrl) {
@@ -284,7 +298,7 @@ function modelOf(context) {
284
298
  const productId = inputs.productIds[done];
285
299
  if (!productId) return aiUnspentOutcome(model);
286
300
  const more = done + 1 < inputs.productIds.length;
287
- const product = await readProductFacts(firestore, site.hostId, productId);
301
+ const product = await readProductFacts(site.hostId, productId);
288
302
  if (!product) {
289
303
  const skipped = {
290
304
  kind: 'copy',
@@ -350,7 +364,7 @@ function modelOf(context) {
350
364
  ]
351
365
  });
352
366
  }
353
- const existing = await readCategories(firestore, site.hostId);
367
+ const existing = await readCategories(site.hostId, deps.categoryIndex);
354
368
  const generation = await generateAiCategories(_extends({
355
369
  store: site.name,
356
370
  brief: job.brief,
@@ -387,6 +401,7 @@ export const runAiJobProductsStep = createAiJobProductsStep();
387
401
  * refusal spends nothing.
388
402
  */ export const aiProductsJobAdmission = async (context)=>{
389
403
  var _data;
404
+ var _pluginRecordIndex;
390
405
  const inputs = parseAiProductsJobInputs(context.inputs);
391
406
  if (typeof inputs === 'string') return {
392
407
  status: 400,
@@ -406,14 +421,21 @@ export const runAiJobProductsStep = createAiJobProductsStep();
406
421
  status: 403,
407
422
  error: AI_PRODUCTS_NOT_ENTITLED_COPY
408
423
  };
424
+ // The plugin that keeps products is whichever publishes their index
425
+ // (AGL-3080); none is the same refusal as one switched off.
426
+ const keeper = (_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.pluginId;
427
+ if (!keeper) return {
428
+ status: 403,
429
+ error: AI_PRODUCTS_COMMERCE_OFF_COPY
430
+ };
409
431
  const host = (_data = (await context.firestore.collection('hosts').doc(context.hostId).get()).data()) != null ? _data : null;
410
432
  const released = await filterEnabledPluginsByReleaseFlags([
411
- COMMERCE_PLUGIN_ID
433
+ keeper
412
434
  ], {
413
435
  orgId: context.orgId,
414
436
  authorization: null
415
437
  });
416
- if (!released.includes(COMMERCE_PLUGIN_ID) || !isHostPluginEnabled(org, host, COMMERCE_PLUGIN_ID)) {
438
+ if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {
417
439
  return {
418
440
  status: 403,
419
441
  error: AI_PRODUCTS_COMMERCE_OFF_COPY
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-step.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/** The commerce plugin, as `plugins.config.json` names it. */\nconst COMMERCE_PLUGIN_ID = 'commerce'\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/** The site's categories by name, as many as a request lists. */\nasync function readCategories(firestore: Firestore, hostId: string): Promise<AiProductCategory[]> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('productCategories')\n .select('name')\n .limit(AI_PRODUCT_CATEGORY_LIST_MAX)\n .get()\n return snapshot.docs\n .map((doc) => ({ id: doc.id, name: str(doc.get('name')) }))\n .filter((category) => category.name)\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/** A saved product's facts, as a bulk pass reads them; `null` when it is gone. */\nasync function readProductFacts(firestore: Firestore, hostId: string, productId: string): Promise<AiProductFacts | null> {\n const snapshot = await firestore.collection('hosts').doc(hostId).collection('products').doc(productId).get()\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n if (!data || data['deletedAt']) return null\n const name = str(data['name'])\n if (!name) return null\n const type = data['type']\n const seo = (data['seo'] ?? {}) as Record<string, unknown>\n const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : []\n return {\n id: productId,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(data['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: (Array.isArray(data['tags']) ? data['tags'] : []).map(str).filter(Boolean),\n categoryIds: (Array.isArray(data['categoryIds']) ? data['categoryIds'] : []).map(str).filter(Boolean),\n options: (Array.isArray(data['options']) ? data['options'] : [])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: (Array.isArray(record['values']) ? record['values'] : [])\n .map(str)\n .filter(Boolean)\n .slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,\n seoTitle: str(seo['title']),\n seoDescription: str(seo['description']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(firestore, site.hostId)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(firestore, site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(firestore, site.hostId)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([COMMERCE_PLUGIN_ID], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(COMMERCE_PLUGIN_ID) || !isHostPluginEnabled(org, host, COMMERCE_PLUGIN_ID)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","COMMERCE_PLUGIN_ID","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","select","limit","docs","map","id","filter","category","sort","a","b","localeCompare","readProductFacts","productId","type","mediaUrls","Array","isArray","includes","text","slice","tags","Boolean","categoryIds","options","option","record","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","photo","image","readsImages","read","status","generation","store","names","Map","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AAE3E,SAASC,mBAAmB,QAAQ,8CAA6C;AACjF,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SAASC,kCAAkC,QAAQ,gDAA+C;AAElG,SACEC,4BAA4B,EAC5BC,sBAAsB,EACtBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,gBAAgB,EAChBC,oBAAoB,EACpBC,sBAAsB,EACtBC,aAAa,EACbC,oBAAoB,EACpBC,wBAAwB,QASnB,0BAAsB;AAC7B,SAASC,aAAa,QAAQ,0BAAsB;AACpD,SAASC,cAAc,QAAQ,0BAAsB;AAErD,SACEC,qBAAqB,EACrBC,gCAAgC,EAChCC,0BAA0B,EAC1BC,gBAAgB,EAChBC,iBAAiB,EACjBC,oBAAoB,EACpBC,qBAAqB,QAChB,uCAAmC;AAC1C,SAASC,kBAAkB,QAAQ,2BAAuB;AAC1D,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,iBAAiB,EAAEC,gBAAgB,QAAQ,yBAAqB;AACzE,SAASC,wBAAwB,EAAEC,kBAAkB,QAAkC,wBAAoB;AAE3G,SAASC,iBAAiB,EAAEC,uBAAuB,QAAQ,eAAW;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED,OAAO,MAAMC,2BAA2B,qDAAoD;AAC5F,OAAO,MAAMC,gCAAgC,yDAAwD;AACrG,OAAO,MAAMC,gCAAgC,4DAA2D;AACxG,OAAO,MAAMC,gCAAgC,0DAAyD;AACtG,OAAO,MAAMC,wBAAwB,4CAA2C;AAChF,OAAO,MAAMC,4BAA4B,qDAAoD;AAE7F,4DAA4D,GAC5D,OAAO,MAAMC,yBAAgF;IAC3F,kBACE;IACFC,YAAY;IACZC,OAAO;AACT,EAAC;AAED,4DAA4D,GAC5D,MAAMC,qBAAqB;AAE3B;;;CAGC,GACD,OAAO,MAAMC,2BAA2B,KAAK;AAE7C,MAAMC,OAAO7B,aAAa,CAACK,iBAAiB;AAE5C;;;;CAIC,GACD,OAAO,MAAMyB,6BAA6BnB,gBAAgB;IACxDoB,MAAMF;IACNG,WAAW5B;IACX6B,SAAS;IACTC,YAAYpB;AACd,GAAE;AAEF,qFAAqF,GACrF,OAAO,MAAMqB,wBAAwBxB,gBAAgB;IACnDoB,MAAMF;IACNG,WAAW9B;IACX+B,SAAS;IACTC,YAAYN;AACd,GAAE;AAEF,8BAA8B,GAC9B,OAAO,MAAMQ,2BAA2BzB,gBAAgB;IACtDoB,MAAMF;IACNG,WAAW7B;IACX8B,SAAS;IACTC,YAAYN;AACd,GAAE;AAEF,2EAA2E,GAC3E,OAAO,MAAMS,8BAA8BP,2BAA0B;AAErE,2EAA2E,GAC3E,OAAO,SAASQ,uBAAuBC,GAA0B;IAC/D,MAAMC,SAASzC,yBAAyBwC,IAAIC,MAAM;IAClD,IAAI,OAAOA,WAAW,UAAU,OAAOV,2BAA2BW,SAAS;IAC3E,IAAID,OAAOE,MAAM,KAAK,WAAW,OAAOP,sBAAsBM,SAAS;IACvE,IAAID,OAAOE,MAAM,KAAK,cAAc,OAAON,yBAAyBK,SAAS;IAC7E,OAAOX,2BAA2BW,SAAS;AAC7C;AAWA,MAAME,MAAM,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAEpF,sEAAsE,GACtE,MAAMC,QAAQ,CAAIF,QAChBG,KAAKC,KAAK,CAACD,KAAKE,SAAS,CAACL;AAE5B,kEAAkE,GAClE,eAAeM,UAAUC,SAAoB,EAAEZ,GAAU;QAQ1Ca;IAPb,IAAI,CAACb,IAAIc,MAAM,EAAE,OAAO;QAAEC,SAASpC;IAAyB;IAC5D,MAAMqC,WAAW,MAAMJ,UAAUK,UAAU,CAAC,SAASC,GAAG,CAAClB,IAAIc,MAAM,EAAEK,GAAG;IACxE,MAAMN,OAAOG,SAASI,MAAM,GAAIJ,SAASK,IAAI,KAAiC;IAC9E,wEAAwE;IACxE,wEAAwE;IACxE,oBAAoB;IACpB,IAAI,CAACR,QAAQA,IAAI,CAAC,QAAQ,KAAKb,IAAIsB,KAAK,EAAE,OAAO;QAAEP,SAASnC;IAA8B;IAC1F,MAAM2C,OAAOV,YAAAA,IAAI,CAAC,MAAM,YAAXA,YAAe,CAAC;IAC7B,MAAMW,YAAYpB,IAAIS,IAAI,CAAC,YAAY,KAAK;IAC5C,OAAO;QACLC,QAAQd,IAAIc,MAAM;QAClBU;QACAC,MAAMrB,IAAImB,GAAG,CAAC,QAAQ,KAAKnB,IAAIS,IAAI,CAAC,cAAc,KAAKW,aAAa;IACtE;AACF;AAEA,+DAA+D,GAC/D,eAAeE,eAAed,SAAoB,EAAEE,MAAc;IAChE,MAAME,WAAW,MAAMJ,UACpBK,UAAU,CAAC,SACXC,GAAG,CAACJ,QACJG,UAAU,CAAC,qBACXU,MAAM,CAAC,QACPC,KAAK,CAAC7E,8BACNoE,GAAG;IACN,OAAOH,SAASa,IAAI,CACjBC,GAAG,CAAC,CAACZ,MAAS,CAAA;YAAEa,IAAIb,IAAIa,EAAE;YAAEN,MAAMrB,IAAIc,IAAIC,GAAG,CAAC;QAAS,CAAA,GACvDa,MAAM,CAAC,CAACC,WAAaA,SAASR,IAAI,EAClCS,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEV,IAAI,CAACY,aAAa,CAACD,EAAEX,IAAI;AAC/C;AAEA,gFAAgF,GAChF,eAAea,iBAAiB1B,SAAoB,EAAEE,MAAc,EAAEyB,SAAiB;QAOxElB;IANb,MAAML,WAAW,MAAMJ,UAAUK,UAAU,CAAC,SAASC,GAAG,CAACJ,QAAQG,UAAU,CAAC,YAAYC,GAAG,CAACqB,WAAWpB,GAAG;IAC1G,MAAME,OAAOL,SAASI,MAAM,GAAIJ,SAASK,IAAI,KAAiC;IAC9E,IAAI,CAACA,QAAQA,IAAI,CAAC,YAAY,EAAE,OAAO;IACvC,MAAMI,OAAOrB,IAAIiB,IAAI,CAAC,OAAO;IAC7B,IAAI,CAACI,MAAM,OAAO;IAClB,MAAMe,OAAOnB,IAAI,CAAC,OAAO;IACzB,MAAME,OAAOF,YAAAA,IAAI,CAAC,MAAM,YAAXA,YAAe,CAAC;IAC7B,MAAMoB,YAAYC,MAAMC,OAAO,CAACtB,IAAI,CAAC,YAAY,IAAIA,IAAI,CAAC,YAAY,GAAG,EAAE;IAC3E,OAAO;QACLU,IAAIQ;QACJd;QACAe,MAAM,AAACrF,iBAAwCyF,QAAQ,CAACJ,QAASA,OAAyB;QAC1FK,MAAMzC,IAAIiB,IAAI,CAAC,cAAc,EAAEyB,KAAK,CAAC,GAAG5F;QACxC6F,MAAM,AAACL,CAAAA,MAAMC,OAAO,CAACtB,IAAI,CAAC,OAAO,IAAIA,IAAI,CAAC,OAAO,GAAG,EAAE,AAAD,EAAGS,GAAG,CAAC1B,KAAK4B,MAAM,CAACgB;QACxEC,aAAa,AAACP,CAAAA,MAAMC,OAAO,CAACtB,IAAI,CAAC,cAAc,IAAIA,IAAI,CAAC,cAAc,GAAG,EAAE,AAAD,EAAGS,GAAG,CAAC1B,KAAK4B,MAAM,CAACgB;QAC7FE,SAAS,AAACR,CAAAA,MAAMC,OAAO,CAACtB,IAAI,CAAC,UAAU,IAAIA,IAAI,CAAC,UAAU,GAAG,EAAE,AAAD,EAC3DyB,KAAK,CAAC,GAAG9F,wBACT8E,GAAG,CAAC,CAACqB;YACJ,MAAMC,SAAUD,iBAAAA,SAAU,CAAC;YAC3B,OAAO;gBACL1B,MAAMrB,IAAIgD,MAAM,CAAC,OAAO;gBACxBC,QAAQ,AAACX,CAAAA,MAAMC,OAAO,CAACS,MAAM,CAAC,SAAS,IAAIA,MAAM,CAAC,SAAS,GAAG,EAAE,AAAD,EAC5DtB,GAAG,CAAC1B,KACJ4B,MAAM,CAACgB,SACPF,KAAK,CAAC,GAAG7F;YACd;QACF,GACC+E,MAAM,CAAC,CAACmB,SAAWA,OAAO1B,IAAI;QACjC6B,UAAUlD,IAAIqC,SAAS,CAAC,EAAE,KAAKrC,IAAIiB,IAAI,CAAC,WAAW,KAAK;QACxDkC,UAAUnD,IAAImB,GAAG,CAAC,QAAQ;QAC1BiC,gBAAgBpD,IAAImB,GAAG,CAAC,cAAc;IACxC;AACF;AAEA,SAASkC,QAAQC,OAAyB;;IACxC,eAAOA,QAAQC,QAAQ,oBAAhBD,QAAQC,QAAQ,MAAhBD,SAAmB5F,oCAAqBJ,eAAeI;AAChE;AAEA,4EAA4E,GAC5E,SAAS8F,OAAOC,IAAe,EAAE9B,EAAU,EAAE+B,KAAa,EAAEC,QAAgB;IAC1E,OAAO;QACLC,UAAU;QACVjC;QACAjB,QAAQ+C,KAAK/C,MAAM;QACnBmD,eAAeJ,KAAKrC,SAAS;QAC7BsC;QACAC,UAAUxD,MAAMwD;IAClB;AACF;AAaA,wEAAwE,GACxE,eAAeG,iBACbR,OAAoD,EACpDvE,KAAa,EACb0E,IAAe,EACfM,OAAuB,EACvBC,IAA2B;;IAE3B,MAAM,EAAEpE,GAAG,EAAEY,SAAS,EAAEyD,MAAM,EAAE,GAAGX;IACnC,MAAMY,aAAa,MAAM5C,eAAed,WAAWiD,KAAK/C,MAAM;IAC9D,IAAIyD,QAA4B;IAChC,IAAIC,QAAQ;IACZ,IAAIL,QAAQb,QAAQ,EAAE;YACfc;QAAL,IAAI,EAACA,oBAAAA,KAAKK,WAAW,YAAhBL,oBAAoBlG,oBAAoBiB,QAAQ;YACnD,MAAMuF,OAAO,MAAMlG,mBACjBoC,WACA;gBAAEP,OAAO8D,QAAQb,QAAQ;gBAAExC,QAAQ+C,KAAK/C,MAAM;gBAAEQ,OAAOtB,IAAIsB,KAAK;YAAC,GACjE8C,KAAKI,KAAK;YAEZD,QAAQG,KAAKC,MAAM;YACnBH,QAAQE,KAAKC,MAAM,KAAK,SAASD,KAAKF,KAAK,GAAG;QAChD,OAAO;YACLD,QAAQ;QACV;IACF;IACA,MAAMK,aAAa,MAAM3G,sBAAsB;QAC7C4G,OAAOhB,KAAKpC,IAAI;QAChB0C;QACAG;QACAC,OAAOvB,QAAQwB;QACfA;QACArF;QACAM,WAAWF,2BAA2BE,SAAS,CAACN;OAC5CkF,SAAS;QAAEA;IAAO,IAAI,CAAC;IAE7B,MAAMhB,SAASuB,WAAWD,MAAM,KAAK,OAAOC,WAAWvE,KAAK,GAAG;IAC/D,MAAMyE,QAAQ,IAAIC,IAAIT,WAAWxC,GAAG,CAAC,CAACG,WAAa;YAACA,SAASF,EAAE;YAAEE,SAASR,IAAI;SAAC;IAC/E,MAAMuD,OAAO/F,sBAAsB,CAACsF,MAAM;IAC1C,OAAO;QACLK;QACAb,UAAU;YACRkB,MAAM;YACNd,SAAS;gBAAEpC,IAAIoC,QAAQpC,EAAE;gBAAEN,MAAM0C,QAAQ1C,IAAI;YAAC;YAC9C4B;YACAiB,YAAY,SAACjB,0BAAAA,OAAQJ,WAAW,mBAAI,EAAE,EAAEnB,GAAG,CAAC,CAACC;oBAAoB+C;uBAAZ;oBAAE/C;oBAAIN,IAAI,GAAEqD,aAAAA,MAAM3D,GAAG,CAACY,eAAV+C,aAAiB/C;gBAAG;;YACrFmD,mBAAmBf,QAAQjB,OAAO,CAACpB,GAAG,CAAC,CAACqB,SAAWA,OAAO1B,IAAI;YAC9D0D,MAAM9B,SAAS/F,cAAc+F,OAAO+B,WAAW,IAAI,EAAE;YACrDb;YACAc,SAAS;YACTC,OAAON,OAAO;gBAACA;aAAK,GAAG,EAAE;QAC3B;IACF;AACF;AAEA,kEAAkE,GAClE,OAAO,SAASO,wBAAwBnB,OAA8B,CAAC,CAAC;IACtE,OAAO,OAAOV;QACZ,MAAM,EAAE1D,GAAG,EAAEY,SAAS,EAAEyD,MAAM,EAAE,GAAGX;QACnC,MAAMvE,QAAQsE,QAAQC;QACtB,MAAMzD,SAASzC,yBAAyBwC,IAAIC,MAAM;QAClD,IAAI,OAAOA,WAAW,UAAU,OAAO,aAAK3B,iBAAiBa;YAAQ4B,SAASd;;QAC9E,MAAM4D,OAAO,MAAMlD,UAAUC,WAAWZ;QACxC,IAAI,aAAa6D,MAAM,OAAO,aAAKvF,iBAAiBa;YAAQ4B,SAAS8C,KAAK9C,OAAO;;QAEjF,IAAId,OAAOE,MAAM,KAAK,WAAW;YAC/B,MAAM,EAAEyE,UAAU,EAAEb,QAAQ,EAAE,GAAG,MAAMG,iBAAiBR,SAASvE,OAAO0E,MAAM5D,OAAOkE,OAAO,EAAEC;YAC9F,MAAMoB,QAAQnH,kBAAkBuG;YAChC,IAAIA,WAAWD,MAAM,KAAK,WAAW,OAAO,aAAKa;gBAAOC,SAAS;;YACjE,IAAIb,WAAWD,MAAM,KAAK,eAAe,OAAO,aAAKa;gBAAOzE,SAAS6D,WAAWc,OAAO;;YACvF,OAAO,aACFF;gBACHG,SAAS;oBAAC/B,OAAOC,MAAMxG,uBAAuBuI,IAAI,CAAC3F,OAAOkE,OAAO,CAACpC,EAAE,GAAG,CAAC,eAAe,EAAE9B,OAAOkE,OAAO,CAAC1C,IAAI,EAAE,EAAEsC;iBAAU;;QAE9H;QAEA,IAAI9D,OAAOE,MAAM,KAAK,QAAQ;gBACdH;YAAd,MAAM6F,OAAO,EAAC7F,eAAAA,IAAI2F,OAAO,YAAX3F,eAAe,EAAE,EAAEgC,MAAM,CAAC,CAAC8D;oBAAUvI;uBAAAA,EAAAA,wBAAAA,qBAAqBuI,2BAArBvI,sBAA6B0H,IAAI,MAAK;eAAQc,MAAM;YACvG,MAAMxD,YAAYtC,OAAO+F,UAAU,CAACH,KAAK;YACzC,IAAI,CAACtD,WAAW,OAAOjE,iBAAiBa;YACxC,MAAM8G,OAAOJ,OAAO,IAAI5F,OAAO+F,UAAU,CAACD,MAAM;YAChD,MAAM5B,UAAU,MAAM7B,iBAAiB1B,WAAWiD,KAAK/C,MAAM,EAAEyB;YAC/D,IAAI,CAAC4B,SAAS;gBACZ,MAAMkB,UAAiC;oBACrCJ,MAAM;oBACNd,SAAS;wBAAEpC,IAAIQ;wBAAWd,MAAMc;oBAAU;oBAC1Cc,QAAQ;oBACRiB,YAAY,EAAE;oBACdY,mBAAmB,EAAE;oBACrBC,MAAM,EAAE;oBACRZ,OAAO;oBACPc,SAAStG;oBACTuG,OAAO,EAAE;gBACX;gBACA,OAAO,aACFhH,iBAAiBa,OAAO;oBACzBwG,SAAS;wBAAC/B,OAAOC,MAAMxG,uBAAuBuI,IAAI,CAACrD,YAAY,CAAC,eAAe,EAAEA,WAAW,EAAE8C;qBAAS;gBACzG;oBACAa,UAAUD;;YAEd;YACA,MAAM,EAAErB,UAAU,EAAEb,QAAQ,EAAE,GAAG,MAAMG,iBAAiBR,SAASvE,OAAO0E,MAAMM,SAASC;YACvF,MAAMoB,QAAQnH,kBAAkBuG;YAChC,kEAAkE;YAClE,sEAAsE;YACtE,8CAA8C;YAC9C,MAAMuB,WACJvB,WAAWD,MAAM,KAAK,OAClBZ,WACA,aACKA;gBACHsB,SAAST,WAAWD,MAAM,KAAK,gBAAgBC,WAAWc,OAAO,GAAG1G;;YAE5E,OAAO,aACFwG;gBACHG,SAAS;oBAAC/B,OAAOC,MAAMxG,uBAAuBuI,IAAI,CAACrD,YAAY,CAAC,eAAe,EAAE4B,QAAQ1C,IAAI,EAAE,EAAE0E;iBAAU;gBAC3GD,UAAUD;;QAEd;QAEA,IAAIhG,OAAOE,MAAM,KAAK,WAAW;YAC/B,MAAMyE,aAAa,MAAM7G,kBAAkB;gBACzC8G,OAAOhB,KAAKpC,IAAI;gBAChB2E,OAAOpG,IAAIoG,KAAK;gBAChBjH;gBACAM,WAAWG,sBAAsBH,SAAS,CAACN;eACvCkF,SAAS;gBAAEA;YAAO,IAAI,CAAC;YAE7B,MAAMmB,QAAQnH,kBAAkBuG;YAChC,IAAIA,WAAWD,MAAM,KAAK,WAAW,OAAO,aAAKa;gBAAOC,SAAS;;YACjE,IAAIb,WAAWD,MAAM,KAAK,eAAe,OAAO,aAAKa;gBAAOzE,SAAS6D,WAAWc,OAAO;;YACvF,MAAM3B,WAA8B;gBAAEkB,MAAM;gBAAWoB,UAAUzB,WAAWvE,KAAK;gBAAEiF,OAAO,EAAE;YAAC;YAC7F,OAAO,aACFE;gBACHG,SAAS;oBACP/B,OACEC,MACAxG,uBAAuBiJ,OAAO,EAC9B,CAAC,oBAAoB,EAAE1B,WAAWvE,KAAK,CAAC0F,MAAM,EAAE,EAChDhC;iBAEH;;QAEL;QAEA,MAAMwC,WAAW,MAAM7E,eAAed,WAAWiD,KAAK/C,MAAM;QAC5D,MAAM8D,aAAa,MAAM5G,qBAAqB;YAC5C6G,OAAOhB,KAAKpC,IAAI;YAChB2E,OAAOpG,IAAIoG,KAAK;YAChBI,uBAAuBD,SAASzE,GAAG,CAAC,CAACG,WAAaA,SAASR,IAAI;YAC/DtC;YACAM,WAAWI,yBAAyBJ,SAAS,CAACN;WAC1CkF,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAMmB,QAAQnH,kBAAkBuG;QAChC,IAAIA,WAAWD,MAAM,KAAK,WAAW,OAAO,aAAKa;YAAOC,SAAS;;QACjE,IAAIb,WAAWD,MAAM,KAAK,eAAe,OAAO,aAAKa;YAAOzE,SAAS6D,WAAWc,OAAO;;QACvF,MAAM3B,WAAiC;YAAEkB,MAAM;WAAiBL,WAAWvE,KAAK;YAAEiF,OAAO,EAAE;;QAC3F,OAAO,aACFE;YACHG,SAAS;gBAAC/B,OAAOC,MAAMxG,uBAAuBiH,UAAU,EAAE,qCAAqCP;aAAU;;IAE7G;AACF;AAEA,OAAO,MAAM0C,uBAAuBlB,0BAAyB;AAE7D;;;;;CAKC,GACD,OAAO,MAAMmB,yBAAyC,OAAOhD;QAQ9C;IAPb,MAAMzD,SAASzC,yBAAyBkG,QAAQzD,MAAM;IACtD,IAAI,OAAOA,WAAW,UAAU,OAAO;QAAE0E,QAAQ;QAAKgC,OAAO1G;IAAO;IACpE,IAAI,CAACyD,QAAQ5C,MAAM,EAAE,OAAO;QAAE6D,QAAQ;QAAKgC,OAAOhI;IAAyB;IAC3E,MAAMiI,QAAQ,MAAM/J,oBAAoB6G,QAAQ5C,MAAM;IACtD,IAAI,CAAC8F,SAASA,UAAUlD,QAAQpC,KAAK,EAAE,OAAO;QAAEqD,QAAQ;QAAKgC,OAAO/H;IAA8B;IAClG,MAAMiI,MAAMnD,QAAQmD,GAAG;IACvB,IAAI,CAAClK,iBAAiBkK,KAAK,aAAa,OAAO;QAAElC,QAAQ;QAAKgC,OAAO9H;IAA8B;IACnG,MAAMgC,QAAO,QAAA,AAAC,CAAA,MAAM6C,QAAQ9C,SAAS,CAACK,UAAU,CAAC,SAASC,GAAG,CAACwC,QAAQ5C,MAAM,EAAEK,GAAG,EAAC,EAAGE,IAAI,cAA5E,QAAkF;IAC/F,MAAMyF,WAAW,MAAMhK,mCAAmC;QAACsC;KAAmB,EAAE;QAC9EkC,OAAOoC,QAAQpC,KAAK;QACpByF,eAAe;IACjB;IACA,IAAI,CAACD,SAASlE,QAAQ,CAACxD,uBAAuB,CAACxC,oBAAoBiK,KAAKhG,MAAMzB,qBAAqB;QACjG,OAAO;YAAEuF,QAAQ;YAAKgC,OAAO7H;QAA8B;IAC7D;IACA,OAAO;AACT,EAAC;AAED;;;CAGC,GACD,OAAO,SAASkI;IACdvI,kBAAkB,YAAYgI,sBAAsB;QAClDvG,WAAWJ,4BAA4BI,SAAS;QAChD+G,cAAclH;IAChB;IACArB,wBAAwB,YAAYtB;IACpCe,uBAAuB,YAAYuI;AACrC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-step.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n pluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/**\n * The site's categories by name, as many as a request lists — from the\n * `productCategory` index the plugin that keeps them publishes (AGL-3080).\n * None when no plugin keeps categories here.\n */\nasync function readCategories(\n hostId: string,\n given?: PluginRecordIndex | null,\n): Promise<AiProductCategory[]> {\n const index = given ?? pluginRecordIndex('productCategory')?.index\n if (!index) return []\n const { records } = await index.list({ hostId, limit: AI_PRODUCT_CATEGORY_LIST_MAX })\n return records\n .map((record) => ({ id: record.id, name: record.name }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * A saved product's facts, as a bulk pass reads them; `null` when it is gone\n * — read through the `product` index the plugin that keeps products publishes\n * (AGL-3080), which answers live, named products only.\n */\nasync function readProductFacts(hostId: string, productId: string): Promise<AiProductFacts | null> {\n const record = await pluginRecordIndex('product')?.index.get({ hostId, id: productId })\n if (!record) return null\n const facts = record.facts\n const type = facts['type']\n const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : [])\n return {\n id: record.id,\n name: record.name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: list(facts['tags']).map(str).filter(Boolean),\n categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),\n options: list(facts['options'])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(facts['imageUrl']) || null,\n seoTitle: str(facts['seoTitle']),\n seoDescription: str(facts['seoDescription']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n /**\n * Where the site's categories are read; the `productCategory` index the\n * plugin that keeps them publishes otherwise. The eval harness hands its\n * own, over the categories a case names.\n */\n categoryIndex?: PluginRecordIndex | null\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(site.hostId, deps.categoryIndex)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(site.hostId, deps.categoryIndex)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n // The plugin that keeps products is whichever publishes their index\n // (AGL-3080); none is the same refusal as one switched off.\n const keeper = pluginRecordIndex('product')?.pluginId\n if (!keeper) return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([keeper], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","pluginRecordIndex","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","given","index","records","list","limit","map","record","id","sort","a","b","localeCompare","readProductFacts","productId","facts","type","Array","isArray","includes","text","slice","tags","filter","Boolean","categoryIds","options","option","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","categoryIndex","photo","image","readsImages","read","status","generation","store","names","Map","category","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","keeper","pluginId","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":"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"}
@@ -19,7 +19,6 @@ import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fiel
19
19
  import type { AiJobOutput } from '../model/ai-jobs.types';
20
20
  import { type AiSeoAuditReport, type AiSeoPageFix, type AiSeoSiteFormField } from '../model/ai-seo';
21
21
  import type { AiSystemBlock } from '../runtime/ai-runtime';
22
- import { type AiSeoAuditPage } from '../runtime/seo-audit';
23
22
  import { type AiSeoBatchAnswer, type AiSeoBatchPage } from '../tools/ai-seo-tool';
24
23
  import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS } from './ai-job-seo-budget';
25
24
  import type { AiJobStepRunner } from './ai-job-text-step';
@@ -48,8 +47,9 @@ import type { AiJobStepRunner } from './ai-job-text-step';
48
47
  * ## An audit, a unit at a time
49
48
  *
50
49
  * The first pass reads the site — every published page's version and the
51
- * shared layouts, for the links between them — scores it without a model,
52
- * records the report, and runs the first unit of generated work. Each later
50
+ * shared layouts, for the links between them — and scores it without a
51
+ * model, through the platform's SEO check (the same findings the site's SEO
52
+ * section lists to every owner); it records the report, and runs the first unit of generated work. Each later
53
53
  * pass runs one more unit: the site-wide proposal, then the page fixes in
54
54
  * batches. Every pass is one reservation and one provider exchange, and it
55
55
  * asks the machine to continue until nothing is left, so a large site is a
@@ -59,8 +59,6 @@ export declare const AI_SEO_NO_SITE_COPY = "This SEO job names a site this works
59
59
  export declare const AI_SEO_NO_PAGE_COPY = "This page is no longer on the site.";
60
60
  export declare const AI_SEO_NO_TARGET_COPY = "This SEO job does not say what to write about.";
61
61
  export declare const AI_SEO_NO_PRODUCT_COPY = "Give the product a name before writing its listing.";
62
- /** Shared layouts an audit reads for the links in a site's navigation. */
63
- export declare const AI_SEO_LAYOUT_SCAN_LIMIT = 20;
64
62
  /** How much of each page a batch of fixes carries. */
65
63
  export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
66
64
  /**
@@ -70,7 +68,6 @@ export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
70
68
  * measured in `ai-job-seo-step.spec.ts`.
71
69
  */
72
70
  export { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS };
73
- type Firestore = FirebaseFirestore.Firestore;
74
71
  /** The host document fields this step reads. */
75
72
  export interface SeoHostDocument {
76
73
  orgId?: string;
@@ -103,24 +100,6 @@ export interface SeoHostDocument {
103
100
  * comma-separated) that the target's editor holds, or all of those.
104
101
  */
105
102
  export declare function aiSeoRequestedFields(raw: unknown, allowed: readonly SeoListingFieldKey[]): SeoListingFieldKey[];
106
- export interface AiSeoScannedPage extends AiSeoAuditPage {
107
- /** The page's node map as audited. */
108
- nodes: Record<string, unknown> | null;
109
- }
110
- export interface AiSeoSiteScan {
111
- pages: AiSeoScannedPage[];
112
- skipped: number;
113
- notes: string[];
114
- }
115
- /**
116
- * Read the site the way its sitemap lists it: every screen the routing map
117
- * publishes, less the template screens, the status pages and every screen
118
- * that is not public — through the same shared predicates the tenant's
119
- * `sitemap.xml` and `/llms.txt` apply, so the audit covers exactly the pages
120
- * a crawler is handed. Then each page's published version, and the shared
121
- * layouts, for the links that decide which pages nothing points to.
122
- */
123
- export declare function scanAiSeoSite(firestore: Firestore, hostId: string, host: SeoHostDocument, rawKeywords: unknown): Promise<AiSeoSiteScan>;
124
103
  /** One unit of generated work an audit still owes. */
125
104
  export type AiSeoAuditUnit = {
126
105
  kind: 'site';