@aglyn/plugins-ai 1.0.0-beta.229 → 1.0.0-beta.231

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 (340) hide show
  1. package/README.md +3 -3
  2. package/package.json +17 -17
  3. package/src/lib/activity/ai-activity-actions.js +7 -1
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-assist-provider-on-host.component.d.ts +66 -0
  6. package/src/lib/components/ai-assist-provider-on-host.component.js +78 -0
  7. package/src/lib/components/ai-assist-provider-on-host.component.js.map +1 -0
  8. package/src/lib/components/ai-assist-provider.component.js +9 -10
  9. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  10. package/src/lib/components/ai-automation-zones.d.ts +38 -0
  11. package/src/lib/components/ai-automation-zones.js +5 -1
  12. package/src/lib/components/ai-automation-zones.js.map +1 -1
  13. package/src/lib/components/ai-brief-dialog.component.d.ts +20 -2
  14. package/src/lib/components/ai-brief-dialog.component.js +87 -8
  15. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  16. package/src/lib/components/ai-campaign-create.component.d.ts +54 -0
  17. package/src/lib/components/ai-campaign-create.component.js +270 -0
  18. package/src/lib/components/ai-campaign-create.component.js.map +1 -0
  19. package/src/lib/components/ai-describe-automation.component.d.ts +8 -3
  20. package/src/lib/components/ai-describe-automation.component.js +15 -6
  21. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  22. package/src/lib/components/ai-describe-button.component.d.ts +20 -8
  23. package/src/lib/components/ai-describe-button.component.js +19 -36
  24. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  25. package/src/lib/components/ai-describe-email.component.d.ts +31 -0
  26. package/src/lib/components/ai-describe-email.component.js +22 -0
  27. package/src/lib/components/ai-describe-email.component.js.map +1 -0
  28. package/src/lib/components/ai-describe-org-automation.component.d.ts +36 -0
  29. package/src/lib/components/ai-describe-org-automation.component.js +238 -0
  30. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -0
  31. package/src/lib/components/ai-experiment-cards.component.d.ts +9 -1
  32. package/src/lib/components/ai-experiment-cards.component.js +104 -18
  33. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  34. package/src/lib/components/ai-explain-automation.component.js +1 -5
  35. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  36. package/src/lib/components/ai-funnel-zones.component.d.ts +61 -0
  37. package/src/lib/components/ai-funnel-zones.component.js +178 -0
  38. package/src/lib/components/ai-funnel-zones.component.js.map +1 -0
  39. package/src/lib/components/ai-insight-card.component.d.ts +42 -0
  40. package/src/lib/components/ai-insight-card.component.js +137 -0
  41. package/src/lib/components/ai-insight-card.component.js.map +1 -0
  42. package/src/lib/components/ai-insight-dialog.component.d.ts +6 -0
  43. package/src/lib/components/ai-insight-dialog.component.js +9 -3
  44. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  45. package/src/lib/components/ai-job-plan.component.d.ts +7 -2
  46. package/src/lib/components/ai-job-plan.component.js +38 -6
  47. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  48. package/src/lib/components/ai-job-requests.d.ts +17 -1
  49. package/src/lib/components/ai-job-requests.js +39 -3
  50. package/src/lib/components/ai-job-requests.js.map +1 -1
  51. package/src/lib/components/ai-logic-zones.d.ts +64 -0
  52. package/src/lib/components/ai-logic-zones.js +18 -0
  53. package/src/lib/components/ai-logic-zones.js.map +1 -0
  54. package/src/lib/components/ai-logic.component.d.ts +77 -0
  55. package/src/lib/components/ai-logic.component.js +553 -0
  56. package/src/lib/components/ai-logic.component.js.map +1 -0
  57. package/src/lib/components/ai-marketing-insight.component.d.ts +30 -0
  58. package/src/lib/components/ai-marketing-insight.component.js +80 -0
  59. package/src/lib/components/ai-marketing-insight.component.js.map +1 -0
  60. package/src/lib/components/ai-media-create.component.d.ts +55 -0
  61. package/src/lib/components/ai-media-create.component.js +433 -0
  62. package/src/lib/components/ai-media-create.component.js.map +1 -0
  63. package/src/lib/components/ai-overlay-cards.component.d.ts +83 -0
  64. package/src/lib/components/ai-overlay-cards.component.js +505 -0
  65. package/src/lib/components/ai-overlay-cards.component.js.map +1 -0
  66. package/src/lib/components/ai-permissions-on-host.component.d.ts +3 -46
  67. package/src/lib/components/ai-permissions-on-host.component.js +5 -53
  68. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  69. package/src/lib/components/ai-product-zones.d.ts +5 -0
  70. package/src/lib/components/ai-product-zones.js +1 -1
  71. package/src/lib/components/ai-product-zones.js.map +1 -1
  72. package/src/lib/components/ai-products-create-button.component.d.ts +42 -0
  73. package/src/lib/components/ai-products-create-button.component.js +67 -0
  74. package/src/lib/components/ai-products-create-button.component.js.map +1 -0
  75. package/src/lib/components/ai-products-hub-card.component.js +100 -81
  76. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  77. package/src/lib/components/ai-revise-automation.component.d.ts +16 -0
  78. package/src/lib/components/ai-revise-automation.component.js +207 -0
  79. package/src/lib/components/ai-revise-automation.component.js.map +1 -0
  80. package/src/lib/components/ai-site-build-page.component.js +160 -24
  81. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  82. package/src/lib/components/ai-upsell-dialog.component.d.ts +53 -0
  83. package/src/lib/components/ai-upsell-dialog.component.js +177 -0
  84. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -0
  85. package/src/lib/components/assist-build-card.component.d.ts +42 -0
  86. package/src/lib/components/assist-build-card.component.js +183 -0
  87. package/src/lib/components/assist-build-card.component.js.map +1 -0
  88. package/src/lib/components/assist-edit-canvas.d.ts +2 -0
  89. package/src/lib/components/assist-edit-canvas.js +56 -1
  90. package/src/lib/components/assist-edit-canvas.js.map +1 -1
  91. package/src/lib/components/assist-jobs-drawer.component.js +48 -3
  92. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  93. package/src/lib/components/assist-panel.component.js +51 -2
  94. package/src/lib/components/assist-panel.component.js.map +1 -1
  95. package/src/lib/components/use-ai-job-run.d.ts +3 -14
  96. package/src/lib/components/use-ai-job-run.js +0 -50
  97. package/src/lib/components/use-ai-job-run.js.map +1 -1
  98. package/src/lib/jobs/ai-build-capabilities.d.ts +58 -0
  99. package/src/lib/jobs/ai-build-capabilities.js +256 -0
  100. package/src/lib/jobs/ai-build-capabilities.js.map +1 -0
  101. package/src/lib/jobs/ai-build-unit-outcome.d.ts +52 -0
  102. package/src/lib/jobs/ai-build-unit-outcome.js +140 -0
  103. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -0
  104. package/src/lib/jobs/ai-job-auto-confirm.d.ts +8 -0
  105. package/src/lib/jobs/ai-job-auto-confirm.js +30 -6
  106. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -1
  107. package/src/lib/jobs/ai-job-build-step.d.ts +144 -0
  108. package/src/lib/jobs/ai-job-build-step.js +668 -0
  109. package/src/lib/jobs/ai-job-build-step.js.map +1 -0
  110. package/src/lib/jobs/ai-job-draft-ids.js +9 -1
  111. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  112. package/src/lib/jobs/ai-job-drafts.d.ts +14 -4
  113. package/src/lib/jobs/ai-job-drafts.js +25 -4
  114. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  115. package/src/lib/jobs/ai-job-generation.d.ts +8 -0
  116. package/src/lib/jobs/ai-job-generation.js +13 -2
  117. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  118. package/src/lib/jobs/ai-job-insight-step.js +8 -1
  119. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  120. package/src/lib/jobs/ai-job-layout-language.d.ts +73 -0
  121. package/src/lib/jobs/ai-job-layout-language.js +194 -0
  122. package/src/lib/jobs/ai-job-layout-language.js.map +1 -0
  123. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +13 -2
  124. package/src/lib/jobs/ai-job-layout-site-pages.js +18 -2
  125. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  126. package/src/lib/jobs/ai-job-layout-step.d.ts +5 -0
  127. package/src/lib/jobs/ai-job-layout-step.js +105 -5
  128. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  129. package/src/lib/jobs/ai-job-logic-step.d.ts +107 -0
  130. package/src/lib/jobs/ai-job-logic-step.js +395 -0
  131. package/src/lib/jobs/ai-job-logic-step.js.map +1 -0
  132. package/src/lib/jobs/ai-job-overlay-copy.d.ts +70 -0
  133. package/src/lib/jobs/ai-job-overlay-copy.js +178 -0
  134. package/src/lib/jobs/ai-job-overlay-copy.js.map +1 -0
  135. package/src/lib/jobs/ai-job-page-language.d.ts +149 -0
  136. package/src/lib/jobs/ai-job-page-language.js +355 -0
  137. package/src/lib/jobs/ai-job-page-language.js.map +1 -0
  138. package/src/lib/jobs/ai-job-page-sections.d.ts +37 -1
  139. package/src/lib/jobs/ai-job-page-sections.js +93 -11
  140. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  141. package/src/lib/jobs/ai-job-page-step.d.ts +4 -4
  142. package/src/lib/jobs/ai-job-page-step.js +111 -20
  143. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  144. package/src/lib/jobs/ai-job-plan-step.d.ts +27 -2
  145. package/src/lib/jobs/ai-job-plan-step.js +87 -13
  146. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  147. package/src/lib/jobs/ai-job-site-step.d.ts +19 -1
  148. package/src/lib/jobs/ai-job-site-step.js +220 -61
  149. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  150. package/src/lib/jobs/ai-job-text-step.d.ts +22 -1
  151. package/src/lib/jobs/ai-job-text-step.js +12 -0
  152. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  153. package/src/lib/jobs/ai-job-workflow-step.d.ts +54 -4
  154. package/src/lib/jobs/ai-job-workflow-step.js +320 -15
  155. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  156. package/src/lib/jobs/ai-jobs.d.ts +80 -2
  157. package/src/lib/jobs/ai-jobs.js +391 -18
  158. package/src/lib/jobs/ai-jobs.js.map +1 -1
  159. package/src/lib/jobs/ai-layout-full-height.d.ts +40 -0
  160. package/src/lib/jobs/ai-layout-full-height.js +82 -0
  161. package/src/lib/jobs/ai-layout-full-height.js.map +1 -0
  162. package/src/lib/jobs/ai-layout-site-facts.d.ts +54 -0
  163. package/src/lib/jobs/ai-layout-site-facts.js +153 -0
  164. package/src/lib/jobs/ai-layout-site-facts.js.map +1 -0
  165. package/src/lib/jobs/ai-logic-records.d.ts +40 -0
  166. package/src/lib/jobs/ai-logic-records.js +55 -0
  167. package/src/lib/jobs/ai-logic-records.js.map +1 -0
  168. package/src/lib/jobs/ai-site-publish.d.ts +7 -0
  169. package/src/lib/jobs/ai-site-publish.js +9 -0
  170. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  171. package/src/lib/jobs/ai-workflow-records.d.ts +4 -0
  172. package/src/lib/jobs/ai-workflow-records.js +40 -1
  173. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  174. package/src/lib/layout-language/ai-layout-compiler.d.ts +145 -0
  175. package/src/lib/layout-language/ai-layout-compiler.js +1148 -0
  176. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -0
  177. package/src/lib/layout-language/ai-layout-copy.d.ts +73 -0
  178. package/src/lib/layout-language/ai-layout-copy.js +137 -0
  179. package/src/lib/layout-language/ai-layout-copy.js.map +1 -0
  180. package/src/lib/layout-language/ai-layout-frame.d.ts +52 -0
  181. package/src/lib/layout-language/ai-layout-frame.js +457 -0
  182. package/src/lib/layout-language/ai-layout-frame.js.map +1 -0
  183. package/src/lib/layout-language/ai-layout-gaps.d.ts +63 -0
  184. package/src/lib/layout-language/ai-layout-gaps.js +345 -0
  185. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -0
  186. package/src/lib/layout-language/ai-layout-language.d.ts +161 -0
  187. package/src/lib/layout-language/ai-layout-language.js +446 -0
  188. package/src/lib/layout-language/ai-layout-language.js.map +1 -0
  189. package/src/lib/layout-language/ai-layout-links.d.ts +80 -0
  190. package/src/lib/layout-language/ai-layout-links.js +109 -0
  191. package/src/lib/layout-language/ai-layout-links.js.map +1 -0
  192. package/src/lib/layout-language/ai-layout-store.d.ts +39 -0
  193. package/src/lib/layout-language/ai-layout-store.js +69 -0
  194. package/src/lib/layout-language/ai-layout-store.js.map +1 -0
  195. package/src/lib/layout-language/ai-layout-tree.d.ts +59 -0
  196. package/src/lib/layout-language/ai-layout-tree.js +84 -0
  197. package/src/lib/layout-language/ai-layout-tree.js.map +1 -0
  198. package/src/lib/lazy-widget.d.ts +45 -0
  199. package/src/lib/lazy-widget.js +53 -0
  200. package/src/lib/lazy-widget.js.map +1 -0
  201. package/src/lib/model/ai-automation-draft.d.ts +18 -0
  202. package/src/lib/model/ai-automation-draft.js +86 -0
  203. package/src/lib/model/ai-automation-draft.js.map +1 -1
  204. package/src/lib/model/ai-build-job.d.ts +201 -0
  205. package/src/lib/model/ai-build-job.js +472 -0
  206. package/src/lib/model/ai-build-job.js.map +1 -0
  207. package/src/lib/model/ai-build-plan.d.ts +61 -0
  208. package/src/lib/model/ai-build-plan.js +154 -9
  209. package/src/lib/model/ai-build-plan.js.map +1 -1
  210. package/src/lib/model/ai-build-progress.d.ts +73 -0
  211. package/src/lib/model/ai-build-progress.js +170 -0
  212. package/src/lib/model/ai-build-progress.js.map +1 -0
  213. package/src/lib/model/ai-host-publish-context.d.ts +51 -0
  214. package/src/lib/model/ai-host-publish-context.js +104 -0
  215. package/src/lib/model/ai-host-publish-context.js.map +1 -0
  216. package/src/lib/model/ai-insight.d.ts +20 -10
  217. package/src/lib/model/ai-insight.js +72 -9
  218. package/src/lib/model/ai-insight.js.map +1 -1
  219. package/src/lib/model/ai-job-activity.d.ts +22 -0
  220. package/src/lib/model/ai-job-activity.js +25 -2
  221. package/src/lib/model/ai-job-activity.js.map +1 -1
  222. package/src/lib/model/ai-job-failure-copy.d.ts +38 -1
  223. package/src/lib/model/ai-job-failure-copy.js +56 -1
  224. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  225. package/src/lib/model/ai-job-notice.d.ts +3 -1
  226. package/src/lib/model/ai-job-notice.js +31 -9
  227. package/src/lib/model/ai-job-notice.js.map +1 -1
  228. package/src/lib/model/ai-jobs.types.d.ts +99 -2
  229. package/src/lib/model/ai-jobs.types.js +3 -1
  230. package/src/lib/model/ai-jobs.types.js.map +1 -1
  231. package/src/lib/model/ai-logic-job.d.ts +108 -0
  232. package/src/lib/model/ai-logic-job.js +327 -0
  233. package/src/lib/model/ai-logic-job.js.map +1 -0
  234. package/src/lib/model/ai-media-credits.d.ts +52 -0
  235. package/src/lib/model/ai-media-credits.js +61 -0
  236. package/src/lib/model/ai-media-credits.js.map +1 -0
  237. package/src/lib/model/ai-overlay-copy.d.ts +114 -0
  238. package/src/lib/model/ai-overlay-copy.js +175 -0
  239. package/src/lib/model/ai-overlay-copy.js.map +1 -0
  240. package/src/lib/model/ai-site-build-progress.d.ts +25 -4
  241. package/src/lib/model/ai-site-build-progress.js +194 -11
  242. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  243. package/src/lib/model/ai-site-job.d.ts +8 -0
  244. package/src/lib/model/ai-site-job.js +9 -0
  245. package/src/lib/model/ai-site-job.js.map +1 -1
  246. package/src/lib/model/ai-site-start-seo.d.ts +1 -1
  247. package/src/lib/model/ai-site-start.d.ts +5 -2
  248. package/src/lib/model/ai-site-start.js +5 -2
  249. package/src/lib/model/ai-site-start.js.map +1 -1
  250. package/src/lib/model/ai-svg.d.ts +69 -0
  251. package/src/lib/model/ai-svg.js +265 -0
  252. package/src/lib/model/ai-svg.js.map +1 -0
  253. package/src/lib/model/ai-usage-by-user.d.ts +3 -2
  254. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  255. package/src/lib/model/ai-workflow-job.d.ts +92 -4
  256. package/src/lib/model/ai-workflow-job.js +124 -7
  257. package/src/lib/model/ai-workflow-job.js.map +1 -1
  258. package/src/lib/model/assist-build.d.ts +58 -0
  259. package/src/lib/model/assist-build.js +52 -0
  260. package/src/lib/model/assist-build.js.map +1 -0
  261. package/src/lib/model/assist-edit.d.ts +48 -3
  262. package/src/lib/model/assist-edit.js +48 -4
  263. package/src/lib/model/assist-edit.js.map +1 -1
  264. package/src/lib/plugin-config.js +2 -0
  265. package/src/lib/plugin-config.js.map +1 -1
  266. package/src/lib/plugin.js +308 -47
  267. package/src/lib/plugin.js.map +1 -1
  268. package/src/lib/providers/catalog.d.ts +82 -5
  269. package/src/lib/providers/catalog.js +83 -12
  270. package/src/lib/providers/catalog.js.map +1 -1
  271. package/src/lib/providers/image-contract.d.ts +116 -0
  272. package/src/lib/providers/image-contract.js +74 -0
  273. package/src/lib/providers/image-contract.js.map +1 -0
  274. package/src/lib/providers/model-choice.js +11 -0
  275. package/src/lib/providers/model-choice.js.map +1 -1
  276. package/src/lib/providers/routing.js +18 -0
  277. package/src/lib/providers/routing.js.map +1 -1
  278. package/src/lib/providers/vertex-image.d.ts +121 -0
  279. package/src/lib/providers/vertex-image.js +312 -0
  280. package/src/lib/providers/vertex-image.js.map +1 -0
  281. package/src/lib/runtime/ai-doctrine-validators.d.ts +101 -0
  282. package/src/lib/runtime/ai-doctrine-validators.js +525 -4
  283. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  284. package/src/lib/runtime/ai-doctrine.js +21 -6
  285. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  286. package/src/lib/runtime/ai-eval-live.js +4 -0
  287. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  288. package/src/lib/runtime/ai-eval.d.ts +15 -1
  289. package/src/lib/runtime/ai-eval.js +52 -1
  290. package/src/lib/runtime/ai-eval.js.map +1 -1
  291. package/src/lib/runtime/ai-icon-library.d.ts +257 -0
  292. package/src/lib/runtime/ai-icon-library.js +254 -0
  293. package/src/lib/runtime/ai-icon-library.js.map +1 -0
  294. package/src/lib/runtime/ai-node-tree.d.ts +9 -0
  295. package/src/lib/runtime/ai-node-tree.js +30 -4
  296. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  297. package/src/lib/runtime/ai-palette.d.ts +6 -0
  298. package/src/lib/runtime/ai-palette.generated.js +70 -0
  299. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  300. package/src/lib/runtime/ai-palette.js.map +1 -1
  301. package/src/lib/runtime/experiment-variant-copy.d.ts +34 -0
  302. package/src/lib/runtime/experiment-variant-copy.js +102 -0
  303. package/src/lib/runtime/experiment-variant-copy.js.map +1 -0
  304. package/src/lib/server/ai-experiment-versions.d.ts +51 -0
  305. package/src/lib/server/ai-experiment-versions.js +270 -0
  306. package/src/lib/server/ai-experiment-versions.js.map +1 -0
  307. package/src/lib/server/ai-jobs-resume.js +74 -4
  308. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  309. package/src/lib/server/ai-media-image.d.ts +66 -0
  310. package/src/lib/server/ai-media-image.js +521 -0
  311. package/src/lib/server/ai-media-image.js.map +1 -0
  312. package/src/lib/server/ai-media-svg.d.ts +91 -0
  313. package/src/lib/server/ai-media-svg.js +186 -0
  314. package/src/lib/server/ai-media-svg.js.map +1 -0
  315. package/src/lib/server/assist-build.d.ts +54 -0
  316. package/src/lib/server/assist-build.js +121 -0
  317. package/src/lib/server/assist-build.js.map +1 -0
  318. package/src/lib/server/assist-chat.js +108 -12
  319. package/src/lib/server/assist-chat.js.map +1 -1
  320. package/src/lib/server/assist-edit.js +213 -3
  321. package/src/lib/server/assist-edit.js.map +1 -1
  322. package/src/lib/server/assist-view-context.js +242 -4
  323. package/src/lib/server/assist-view-context.js.map +1 -1
  324. package/src/lib/server.js +26 -0
  325. package/src/lib/server.js.map +1 -1
  326. package/src/lib/tools/ai-logic-tool.d.ts +27 -0
  327. package/src/lib/tools/ai-logic-tool.js +207 -0
  328. package/src/lib/tools/ai-logic-tool.js.map +1 -0
  329. package/src/lib/tools/ai-overlay-tool.d.ts +35 -0
  330. package/src/lib/tools/ai-overlay-tool.js +107 -0
  331. package/src/lib/tools/ai-overlay-tool.js.map +1 -0
  332. package/src/lib/tools/ai-workflow-tool.d.ts +17 -2
  333. package/src/lib/tools/ai-workflow-tool.js +47 -14
  334. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  335. package/src/lib/usage/assist-job-refund.d.ts +28 -1
  336. package/src/lib/usage/assist-job-refund.js +44 -9
  337. package/src/lib/usage/assist-job-refund.js.map +1 -1
  338. package/src/lib/usage/assist-usage.d.ts +5 -0
  339. package/src/lib/usage/assist-usage.js +2 -0
  340. package/src/lib/usage/assist-usage.js.map +1 -1
@@ -17,12 +17,20 @@
17
17
  'analytics',
18
18
  'datasets',
19
19
  'crm-reports',
20
+ 'marketing',
21
+ 'automations',
22
+ 'bookings',
23
+ 'workspace',
20
24
  'digest'
21
25
  ];
22
26
  /** The surfaces a person asks from. */ export const AI_INSIGHT_ASK_SURFACES = [
23
27
  'analytics',
24
28
  'datasets',
25
- 'crm-reports'
29
+ 'crm-reports',
30
+ 'marketing',
31
+ 'automations',
32
+ 'bookings',
33
+ 'workspace'
26
34
  ];
27
35
  /** The surface an insight job's inputs name, or `null` for one no insight job is asked from. */ export function aiInsightSurface(inputs) {
28
36
  const surface = inputs == null ? void 0 : inputs['surface'];
@@ -30,14 +38,23 @@
30
38
  }
31
39
  /**
32
40
  * The readers a surface may offer, by the first word of a reader's id. The
33
- * Analytics page answers about the site's traffic and what it sold, took and
34
- * collected; a Data page about its datasets; CRM Reports about where people
35
- * came from and what they did.
41
+ * Analytics page — and a site's dashboard — answers about the site's traffic,
42
+ * what it sold, took and collected, its automations' runs and its CRM
43
+ * pipeline; a Data page about its datasets; CRM Reports about where people
44
+ * came from, what they did and the pipeline; the Marketing page about what
45
+ * its campaigns, overlays and tests caused and earned, beside the traffic and
46
+ * the forms they drove; the Automation page about its runs; the Bookings page
47
+ * about its bookings; and the workspace's own pages about its pipeline and
48
+ * its datasets.
36
49
  */ export const AI_INSIGHT_SURFACE_READERS = {
50
+ // `funnels` (AGL-3605): a site's funnels, step by step, beside its traffic.
37
51
  analytics: [
52
+ 'automations',
38
53
  'bookings',
39
54
  'commerce',
55
+ 'crm',
40
56
  'forms',
57
+ 'funnels',
41
58
  'marketing',
42
59
  'traffic'
43
60
  ],
@@ -46,11 +63,28 @@
46
63
  ],
47
64
  'crm-reports': [
48
65
  'bookings',
66
+ 'commerce',
67
+ 'crm',
68
+ 'forms',
69
+ 'marketing',
70
+ 'traffic'
71
+ ],
72
+ marketing: [
49
73
  'commerce',
50
74
  'forms',
51
75
  'marketing',
52
76
  'traffic'
53
77
  ],
78
+ automations: [
79
+ 'automations'
80
+ ],
81
+ bookings: [
82
+ 'bookings'
83
+ ],
84
+ workspace: [
85
+ 'crm',
86
+ 'datasets'
87
+ ],
54
88
  digest: [
55
89
  'bookings',
56
90
  'commerce',
@@ -59,8 +93,8 @@
59
93
  'traffic'
60
94
  ]
61
95
  };
62
- /** Whether a surface answers about one site; the Data page may answer about the whole workspace. */ export function aiInsightSurfaceNeedsSite(surface) {
63
- return surface !== 'datasets';
96
+ /** Whether a surface answers about one site; the Data page and the workspace's pages may answer about the whole workspace. */ export function aiInsightSurfaceNeedsSite(surface) {
97
+ return surface !== 'datasets' && surface !== 'workspace';
64
98
  }
65
99
  /**
66
100
  * The readers a weekly digest reads, in the order the digest names them:
@@ -115,9 +149,12 @@
115
149
  };
116
150
  }
117
151
  /**
118
- * The Assist panel's insight surface for a console path, or `null` off one.
119
- * A site's Analytics, Data and CRM Reports pages, and the workspace's Data
120
- * page; the workspace's CRM Reports reads site figures, so it is not one.
152
+ * The Assist panel's insight surface for a console path, or `null` off one:
153
+ * a site's dashboard, Analytics, Data, CRM Reports, Marketing, Automation and
154
+ * Bookings pages; the workspace's Data page; and the workspace's sites page
155
+ * and CRM Reports, which read the workspace's pipeline and datasets. The
156
+ * organization's Marketing page reads site figures, so it is not one — it
157
+ * asks about the site it is showing, through its own in-page entry.
121
158
  */ export function aiInsightSurfaceForPath(pathname) {
122
159
  const segments = String(pathname != null ? pathname : '').split('?')[0].split('/').filter(Boolean);
123
160
  // `/{org}/hosts/{host}/…` on a site, `/{org}/…` on the workspace.
@@ -136,12 +173,38 @@
136
173
  surface: 'crm-reports',
137
174
  host
138
175
  };
176
+ if (rest[0] === 'marketing') return {
177
+ surface: 'marketing',
178
+ host
179
+ };
180
+ if (rest[0] === 'automation') return {
181
+ surface: 'automations',
182
+ host
183
+ };
184
+ if (rest[0] === 'bookings') return {
185
+ surface: 'bookings',
186
+ host
187
+ };
139
188
  return null;
140
189
  }
190
+ // The site's dashboard is `/{org}/hosts/{host}`.
191
+ if (segments[1] === 'hosts' && segments.length === 3) return {
192
+ surface: 'analytics',
193
+ host: segments[2]
194
+ };
141
195
  if (segments.length >= 2 && segments[1] === 'data') return {
142
196
  surface: 'datasets',
143
197
  host: null
144
198
  };
199
+ // The workspace's sites page, `/{org}/hosts`, which carries its dashboard row.
200
+ if (segments.length === 2 && segments[1] === 'hosts') return {
201
+ surface: 'workspace',
202
+ host: null
203
+ };
204
+ if (segments[1] === 'crm' && segments[2] === 'reports') return {
205
+ surface: 'workspace',
206
+ host: null
207
+ };
145
208
  return null;
146
209
  }
147
210
  /** The console page a table's figures come from, or `null` when the table names none. */ export function aiInsightSourceHref(table, where) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-insight.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 type { PluginFigureTable } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport type { AiJob, AiJobOutput } from './ai-jobs.types'\n\n/**\n * Insights (AGL-2915): what an `insight` job answers, in the shape the step\n * writes and the console reads. Pure data — no server import — so the answer\n * card and the digest email type what they render from the module the step\n * writes it with.\n *\n * ## A number is read, never written\n *\n * The model never runs a query and never sees a record. Code reads figures —\n * traffic, orders, bookings, forms, campaigns, datasets, experiments — through\n * fixed, typed readers, each answering with one compact table that says where\n * its figures come from. The model picks the readers and phrases what their\n * tables say, and every insight it writes cites the rows its figures come from.\n * An insight whose numbers cannot be found in the rows it cites is left out\n * (`runtime/ai-insight-check.ts`), so each one a person reads can be traced to\n * a number they can open in the console.\n *\n * ## Read-only, and kept apart from the job\n *\n * An insight job writes one document besides its own record: the answer, at\n * `orgs/{orgId}/aiInsights/{jobId}`, which no rule lets a client read. The\n * job's output names it and carries no figure, because a job document is\n * readable by every member of the workspace and the figures are not: a\n * collaborator on one site may not see another site's revenue. The answer is\n * served by `GET /api/ai/insights/{jobId}` to the member who asked and, for a\n * weekly digest, to the members who reach its site.\n */\n\n/**\n * Where an insight was asked: the console page whose Assist panel asked it,\n * or the weekly digest, which no person asked.\n */\nexport type AiInsightSurface = 'analytics' | 'datasets' | 'crm-reports' | 'digest'\n\nexport const AI_INSIGHT_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'digest',\n]\n\n/** The surfaces a person asks from. */\nexport const AI_INSIGHT_ASK_SURFACES: readonly AiInsightSurface[] = ['analytics', 'datasets', 'crm-reports']\n\n/** The surface an insight job's inputs name, or `null` for one no insight job is asked from. */\nexport function aiInsightSurface(inputs: Readonly<Record<string, unknown>> | null | undefined): AiInsightSurface | null {\n const surface = inputs?.['surface']\n return typeof surface === 'string' && (AI_INSIGHT_SURFACES as readonly string[]).includes(surface)\n ? (surface as AiInsightSurface)\n : null\n}\n\n/**\n * The readers a surface may offer, by the first word of a reader's id. The\n * Analytics page answers about the site's traffic and what it sold, took and\n * collected; a Data page about its datasets; CRM Reports about where people\n * came from and what they did.\n */\nexport const AI_INSIGHT_SURFACE_READERS: Readonly<Record<AiInsightSurface, readonly string[]>> = {\n analytics: ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n datasets: ['datasets'],\n 'crm-reports': ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n digest: ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n}\n\n/** Whether a surface answers about one site; the Data page may answer about the whole workspace. */\nexport function aiInsightSurfaceNeedsSite(surface: AiInsightSurface): boolean {\n return surface !== 'datasets'\n}\n\n/**\n * The readers a weekly digest reads, in the order the digest names them:\n * the traffic change, the top page, the days that stood out, the forms, the\n * campaigns, and what the site sold and took. Code reads these; no model\n * picks them.\n */\nexport const AI_INSIGHT_DIGEST_READERS: readonly string[] = [\n 'traffic.summary',\n 'traffic.pages',\n 'traffic.daily',\n 'forms.performance',\n 'marketing.campaigns',\n 'commerce.sales',\n 'bookings.services',\n]\n\n/** The windows, in days, an insight reads: the Traffic card's own ranges. */\nexport const AI_INSIGHT_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** The window a question that names none reads: the Traffic card's default. */\nexport const AI_INSIGHT_DEFAULT_DAYS = 14\n\n/** The window a weekly digest reads. */\nexport const AI_INSIGHT_DIGEST_DAYS = 7\n\n/** A window an insight's inputs or a read names, held to the readers' own ranges. */\nexport function aiInsightDays(value: unknown): number {\n const days = typeof value === 'number' ? value : Number(value)\n return AI_INSIGHT_WINDOWS.includes(days) ? days : AI_INSIGHT_DEFAULT_DAYS\n}\n\n/** The most insights one answer carries: a person reads them at a glance. */\nexport const AI_INSIGHT_MAX_INSIGHTS = 5\n\n/** The longest one insight may run, in characters. */\nexport const AI_INSIGHT_MAX_CHARS = 280\n\n/** The longest the sentence about what the figures could not answer may run. */\nexport const AI_INSIGHT_GAP_MAX_CHARS = 200\n\n/** The most tables one citation list names. */\nexport const AI_INSIGHT_MAX_CITES = 3\n\n/** The most rows one citation names. */\nexport const AI_INSIGHT_MAX_CITED_ROWS = 10\n\n/** The most readers one answer reads. */\nexport const AI_INSIGHT_MAX_READS = 4\n\n/** The longest question an insight job admits. */\nexport const AI_INSIGHT_QUESTION_MAX_CHARS = 1_000\n\n/**\n * A table as an insight job keeps it: the figures a reader returned, the\n * handle the model cites them by, and the reader they came from.\n */\nexport interface AiInsightTable extends PluginFigureTable {\n /** `t1`, `t2`, …, in the order the job read them. */\n ref: string\n /** The reader's id (`commerce.sales`). */\n reader: string\n /** The window the reader was asked for, in days; 0 for a reader of current counts. */\n days: number\n /** Whether the table is about one site (its path under that site) or the workspace. */\n scope: 'site' | 'org'\n}\n\n/** The rows of one table an insight's figures come from, by index. */\nexport interface AiInsightCitation {\n table: string\n rows: number[]\n}\n\nexport interface AiInsight {\n /** One or two plain sentences. */\n text: string\n cites: AiInsightCitation[]\n}\n\n/** What the model answers with. */\nexport interface AiInsightAnswer {\n insights: AiInsight[]\n /** What the figures could not answer, in one sentence without numbers; `null` when nothing. */\n gap: string | null\n}\n\n/** The answer collection under the org: server-written, and read through the insights door alone. */\nexport const AI_INSIGHTS_COLLECTION = 'aiInsights'\n\n/** An insight job's answer, as the step stores it. */\nexport interface AiInsightRecord {\n jobId: string\n orgId: string\n hostId: string | null\n surface: AiInsightSurface\n /** The member whose question it answers; for a digest, the member it was made for. */\n createdBy: string\n /** The question, verbatim; a digest's is the digest's own title. */\n question: string\n /** The window the answer was asked about, in days. */\n days: number\n /** The insights that trace to the rows they cite; every other one was left out. */\n insights: AiInsight[]\n gap: string | null\n /** Every table the job read, so each citation opens the figures it names. */\n tables: AiInsightTable[]\n /** How many insights the answer carried that were left out as not traceable. */\n left: number\n /** The ISO week a digest covers (`2026-W38`); `null` on a question. */\n week: string | null\n createdAtMs: number\n}\n\n/** The output id an answer is named by. */\nexport const AI_INSIGHT_OUTPUT_LABEL = 'Answer'\n\n/**\n * The output an insight job records: the answer's address and how many\n * insights it holds, and no figure. The count is a statement about the\n * answer rather than about the site, so every member may read it.\n */\nexport function aiInsightOutput(\n job: Pick<AiJob, '$id' | 'hostId'>,\n record: Pick<AiInsightRecord, 'insights' | 'surface'>,\n hostSubdomain: string | null,\n): AiJobOutput {\n const count = record.insights.length\n return {\n resource: 'insight',\n id: job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain,\n label: record.surface === 'digest' ? 'Weekly insights' : AI_INSIGHT_OUTPUT_LABEL,\n note: count\n ? `${count} ${count === 1 ? 'insight' : 'insights'}, each traced to the figures it cites.`\n : 'The figures did not answer this question.',\n }\n}\n\n/** The answer on the wire: the stored record, with nothing a client does not render. */\nexport type AiInsightAnswerWire = AiInsightRecord\n\n/**\n * The Assist panel's insight surface for a console path, or `null` off one.\n * A site's Analytics, Data and CRM Reports pages, and the workspace's Data\n * page; the workspace's CRM Reports reads site figures, so it is not one.\n */\nexport function aiInsightSurfaceForPath(\n pathname: string | null | undefined,\n): { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null {\n const segments = String(pathname ?? '')\n .split('?')[0]\n .split('/')\n .filter(Boolean)\n // `/{org}/hosts/{host}/…` on a site, `/{org}/…` on the workspace.\n if (segments[1] === 'hosts' && segments.length >= 4) {\n const host = segments[2]\n const rest = segments.slice(3)\n if (rest[0] === 'analytics') return { surface: 'analytics', host }\n if (rest[0] === 'data') return { surface: 'datasets', host }\n if (rest[0] === 'crm' && rest[1] === 'reports') return { surface: 'crm-reports', host }\n return null\n }\n if (segments.length >= 2 && segments[1] === 'data') return { surface: 'datasets', host: null }\n return null\n}\n\n/** The console page a table's figures come from, or `null` when the table names none. */\nexport function aiInsightSourceHref(\n table: Pick<AiInsightTable, 'source' | 'scope'>,\n where: { orgSlug: string; host: string | null },\n): string | null {\n const path = table.source.path\n if (!path || !where.orgSlug) return null\n if (table.scope === 'org') return `/${where.orgSlug}/${path}`\n return where.host ? `/${where.orgSlug}/hosts/${where.host}/${path}` : null\n}\n\n/** How a cell reads to a person, by its column's kind; `null` is \"—\". */\nexport function aiInsightCellText(\n table: Pick<AiInsightTable, 'columns'>,\n key: string,\n value: string | number | null | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** One cited row as a line a person reads: every column, labeled. */\nexport function aiInsightRowText(table: Pick<AiInsightTable, 'columns' | 'rows'>, index: number): string {\n const row = table.rows[index]\n if (!row) return ''\n return table.columns\n .map((column) => `${column.label}: ${aiInsightCellText(table, column.key, row[column.key])}`)\n .join(' · ')\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function aiInsightIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n"],"names":["AI_INSIGHT_SURFACES","AI_INSIGHT_ASK_SURFACES","aiInsightSurface","inputs","surface","includes","AI_INSIGHT_SURFACE_READERS","analytics","datasets","digest","aiInsightSurfaceNeedsSite","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_WINDOWS","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_DIGEST_DAYS","aiInsightDays","value","days","Number","AI_INSIGHT_MAX_INSIGHTS","AI_INSIGHT_MAX_CHARS","AI_INSIGHT_GAP_MAX_CHARS","AI_INSIGHT_MAX_CITES","AI_INSIGHT_MAX_CITED_ROWS","AI_INSIGHT_MAX_READS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","AI_INSIGHT_OUTPUT_LABEL","aiInsightOutput","job","record","hostSubdomain","count","insights","length","resource","id","$id","versionId","hostId","label","note","aiInsightSurfaceForPath","pathname","segments","String","split","filter","Boolean","host","rest","slice","aiInsightSourceHref","table","where","path","source","orgSlug","scope","aiInsightCellText","key","undefined","column","columns","find","entry","kind","Intl","NumberFormat","style","currency","format","toLocaleString","maximumFractionDigits","seconds","Math","round","minutes","floor","padStart","aiInsightRowText","index","row","rows","map","join","aiInsightIsoWeek","at","date","Date","UTC","getUTCFullYear","getUTCMonth","getUTCDate","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-insight.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 type { PluginFigureTable } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport type { AiJob, AiJobOutput } from './ai-jobs.types'\n\n/**\n * Insights (AGL-2915): what an `insight` job answers, in the shape the step\n * writes and the console reads. Pure data — no server import — so the answer\n * card and the digest email type what they render from the module the step\n * writes it with.\n *\n * ## A number is read, never written\n *\n * The model never runs a query and never sees a record. Code reads figures —\n * traffic, orders, bookings, forms, campaigns, datasets, experiments — through\n * fixed, typed readers, each answering with one compact table that says where\n * its figures come from. The model picks the readers and phrases what their\n * tables say, and every insight it writes cites the rows its figures come from.\n * An insight whose numbers cannot be found in the rows it cites is left out\n * (`runtime/ai-insight-check.ts`), so each one a person reads can be traced to\n * a number they can open in the console.\n *\n * ## Read-only, and kept apart from the job\n *\n * An insight job writes one document besides its own record: the answer, at\n * `orgs/{orgId}/aiInsights/{jobId}`, which no rule lets a client read. The\n * job's output names it and carries no figure, because a job document is\n * readable by every member of the workspace and the figures are not: a\n * collaborator on one site may not see another site's revenue. The answer is\n * served by `GET /api/ai/insights/{jobId}` to the member who asked and, for a\n * weekly digest, to the members who reach its site.\n */\n\n/**\n * Where an insight was asked: the console page whose Assist panel — or\n * whose \"Ask AI about these numbers\" card — asked it, or the weekly digest,\n * which no person asked. A site's Marketing, Automation and Bookings pages\n * and the workspace's own pages joined at AGL-3603.\n */\nexport type AiInsightSurface =\n | 'analytics'\n | 'datasets'\n | 'crm-reports'\n | 'marketing'\n | 'automations'\n | 'bookings'\n | 'workspace'\n | 'digest'\n\nexport const AI_INSIGHT_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n 'digest',\n]\n\n/** The surfaces a person asks from. */\nexport const AI_INSIGHT_ASK_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n]\n\n/** The surface an insight job's inputs name, or `null` for one no insight job is asked from. */\nexport function aiInsightSurface(inputs: Readonly<Record<string, unknown>> | null | undefined): AiInsightSurface | null {\n const surface = inputs?.['surface']\n return typeof surface === 'string' && (AI_INSIGHT_SURFACES as readonly string[]).includes(surface)\n ? (surface as AiInsightSurface)\n : null\n}\n\n/**\n * The readers a surface may offer, by the first word of a reader's id. The\n * Analytics page — and a site's dashboard — answers about the site's traffic,\n * what it sold, took and collected, its automations' runs and its CRM\n * pipeline; a Data page about its datasets; CRM Reports about where people\n * came from, what they did and the pipeline; the Marketing page about what\n * its campaigns, overlays and tests caused and earned, beside the traffic and\n * the forms they drove; the Automation page about its runs; the Bookings page\n * about its bookings; and the workspace's own pages about its pipeline and\n * its datasets.\n */\nexport const AI_INSIGHT_SURFACE_READERS: Readonly<Record<AiInsightSurface, readonly string[]>> = {\n // `funnels` (AGL-3605): a site's funnels, step by step, beside its traffic.\n analytics: ['automations', 'bookings', 'commerce', 'crm', 'forms', 'funnels', 'marketing', 'traffic'],\n datasets: ['datasets'],\n 'crm-reports': ['bookings', 'commerce', 'crm', 'forms', 'marketing', 'traffic'],\n marketing: ['commerce', 'forms', 'marketing', 'traffic'],\n automations: ['automations'],\n bookings: ['bookings'],\n workspace: ['crm', 'datasets'],\n digest: ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n}\n\n/** Whether a surface answers about one site; the Data page and the workspace's pages may answer about the whole workspace. */\nexport function aiInsightSurfaceNeedsSite(surface: AiInsightSurface): boolean {\n return surface !== 'datasets' && surface !== 'workspace'\n}\n\n/**\n * The readers a weekly digest reads, in the order the digest names them:\n * the traffic change, the top page, the days that stood out, the forms, the\n * campaigns, and what the site sold and took. Code reads these; no model\n * picks them.\n */\nexport const AI_INSIGHT_DIGEST_READERS: readonly string[] = [\n 'traffic.summary',\n 'traffic.pages',\n 'traffic.daily',\n 'forms.performance',\n 'marketing.campaigns',\n 'commerce.sales',\n 'bookings.services',\n]\n\n/** The windows, in days, an insight reads: the Traffic card's own ranges. */\nexport const AI_INSIGHT_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** The window a question that names none reads: the Traffic card's default. */\nexport const AI_INSIGHT_DEFAULT_DAYS = 14\n\n/** The window a weekly digest reads. */\nexport const AI_INSIGHT_DIGEST_DAYS = 7\n\n/** A window an insight's inputs or a read names, held to the readers' own ranges. */\nexport function aiInsightDays(value: unknown): number {\n const days = typeof value === 'number' ? value : Number(value)\n return AI_INSIGHT_WINDOWS.includes(days) ? days : AI_INSIGHT_DEFAULT_DAYS\n}\n\n/** The most insights one answer carries: a person reads them at a glance. */\nexport const AI_INSIGHT_MAX_INSIGHTS = 5\n\n/** The longest one insight may run, in characters. */\nexport const AI_INSIGHT_MAX_CHARS = 280\n\n/** The longest the sentence about what the figures could not answer may run. */\nexport const AI_INSIGHT_GAP_MAX_CHARS = 200\n\n/** The most tables one citation list names. */\nexport const AI_INSIGHT_MAX_CITES = 3\n\n/** The most rows one citation names. */\nexport const AI_INSIGHT_MAX_CITED_ROWS = 10\n\n/** The most readers one answer reads. */\nexport const AI_INSIGHT_MAX_READS = 4\n\n/** The longest question an insight job admits. */\nexport const AI_INSIGHT_QUESTION_MAX_CHARS = 1_000\n\n/**\n * A table as an insight job keeps it: the figures a reader returned, the\n * handle the model cites them by, and the reader they came from.\n */\nexport interface AiInsightTable extends PluginFigureTable {\n /** `t1`, `t2`, …, in the order the job read them. */\n ref: string\n /** The reader's id (`commerce.sales`). */\n reader: string\n /** The window the reader was asked for, in days; 0 for a reader of current counts. */\n days: number\n /** Whether the table is about one site (its path under that site) or the workspace. */\n scope: 'site' | 'org'\n}\n\n/** The rows of one table an insight's figures come from, by index. */\nexport interface AiInsightCitation {\n table: string\n rows: number[]\n}\n\nexport interface AiInsight {\n /** One or two plain sentences. */\n text: string\n cites: AiInsightCitation[]\n}\n\n/** What the model answers with. */\nexport interface AiInsightAnswer {\n insights: AiInsight[]\n /** What the figures could not answer, in one sentence without numbers; `null` when nothing. */\n gap: string | null\n}\n\n/** The answer collection under the org: server-written, and read through the insights door alone. */\nexport const AI_INSIGHTS_COLLECTION = 'aiInsights'\n\n/** An insight job's answer, as the step stores it. */\nexport interface AiInsightRecord {\n jobId: string\n orgId: string\n hostId: string | null\n surface: AiInsightSurface\n /** The member whose question it answers; for a digest, the member it was made for. */\n createdBy: string\n /** The question, verbatim; a digest's is the digest's own title. */\n question: string\n /** The window the answer was asked about, in days. */\n days: number\n /** The insights that trace to the rows they cite; every other one was left out. */\n insights: AiInsight[]\n gap: string | null\n /** Every table the job read, so each citation opens the figures it names. */\n tables: AiInsightTable[]\n /** How many insights the answer carried that were left out as not traceable. */\n left: number\n /** The ISO week a digest covers (`2026-W38`); `null` on a question. */\n week: string | null\n createdAtMs: number\n}\n\n/** The output id an answer is named by. */\nexport const AI_INSIGHT_OUTPUT_LABEL = 'Answer'\n\n/**\n * The output an insight job records: the answer's address and how many\n * insights it holds, and no figure. The count is a statement about the\n * answer rather than about the site, so every member may read it.\n */\nexport function aiInsightOutput(\n job: Pick<AiJob, '$id' | 'hostId'>,\n record: Pick<AiInsightRecord, 'insights' | 'surface'>,\n hostSubdomain: string | null,\n): AiJobOutput {\n const count = record.insights.length\n return {\n resource: 'insight',\n id: job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain,\n label: record.surface === 'digest' ? 'Weekly insights' : AI_INSIGHT_OUTPUT_LABEL,\n note: count\n ? `${count} ${count === 1 ? 'insight' : 'insights'}, each traced to the figures it cites.`\n : 'The figures did not answer this question.',\n }\n}\n\n/** The answer on the wire: the stored record, with nothing a client does not render. */\nexport type AiInsightAnswerWire = AiInsightRecord\n\n/**\n * The Assist panel's insight surface for a console path, or `null` off one:\n * a site's dashboard, Analytics, Data, CRM Reports, Marketing, Automation and\n * Bookings pages; the workspace's Data page; and the workspace's sites page\n * and CRM Reports, which read the workspace's pipeline and datasets. The\n * organization's Marketing page reads site figures, so it is not one — it\n * asks about the site it is showing, through its own in-page entry.\n */\nexport function aiInsightSurfaceForPath(\n pathname: string | null | undefined,\n): { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null {\n const segments = String(pathname ?? '')\n .split('?')[0]\n .split('/')\n .filter(Boolean)\n // `/{org}/hosts/{host}/…` on a site, `/{org}/…` on the workspace.\n if (segments[1] === 'hosts' && segments.length >= 4) {\n const host = segments[2]\n const rest = segments.slice(3)\n if (rest[0] === 'analytics') return { surface: 'analytics', host }\n if (rest[0] === 'data') return { surface: 'datasets', host }\n if (rest[0] === 'crm' && rest[1] === 'reports') return { surface: 'crm-reports', host }\n if (rest[0] === 'marketing') return { surface: 'marketing', host }\n if (rest[0] === 'automation') return { surface: 'automations', host }\n if (rest[0] === 'bookings') return { surface: 'bookings', host }\n return null\n }\n // The site's dashboard is `/{org}/hosts/{host}`.\n if (segments[1] === 'hosts' && segments.length === 3) return { surface: 'analytics', host: segments[2] }\n if (segments.length >= 2 && segments[1] === 'data') return { surface: 'datasets', host: null }\n // The workspace's sites page, `/{org}/hosts`, which carries its dashboard row.\n if (segments.length === 2 && segments[1] === 'hosts') return { surface: 'workspace', host: null }\n if (segments[1] === 'crm' && segments[2] === 'reports') return { surface: 'workspace', host: null }\n return null\n}\n\n/** The console page a table's figures come from, or `null` when the table names none. */\nexport function aiInsightSourceHref(\n table: Pick<AiInsightTable, 'source' | 'scope'>,\n where: { orgSlug: string; host: string | null },\n): string | null {\n const path = table.source.path\n if (!path || !where.orgSlug) return null\n if (table.scope === 'org') return `/${where.orgSlug}/${path}`\n return where.host ? `/${where.orgSlug}/hosts/${where.host}/${path}` : null\n}\n\n/** How a cell reads to a person, by its column's kind; `null` is \"—\". */\nexport function aiInsightCellText(\n table: Pick<AiInsightTable, 'columns'>,\n key: string,\n value: string | number | null | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** One cited row as a line a person reads: every column, labeled. */\nexport function aiInsightRowText(table: Pick<AiInsightTable, 'columns' | 'rows'>, index: number): string {\n const row = table.rows[index]\n if (!row) return ''\n return table.columns\n .map((column) => `${column.label}: ${aiInsightCellText(table, column.key, row[column.key])}`)\n .join(' · ')\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function aiInsightIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n"],"names":["AI_INSIGHT_SURFACES","AI_INSIGHT_ASK_SURFACES","aiInsightSurface","inputs","surface","includes","AI_INSIGHT_SURFACE_READERS","analytics","datasets","marketing","automations","bookings","workspace","digest","aiInsightSurfaceNeedsSite","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_WINDOWS","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_DIGEST_DAYS","aiInsightDays","value","days","Number","AI_INSIGHT_MAX_INSIGHTS","AI_INSIGHT_MAX_CHARS","AI_INSIGHT_GAP_MAX_CHARS","AI_INSIGHT_MAX_CITES","AI_INSIGHT_MAX_CITED_ROWS","AI_INSIGHT_MAX_READS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","AI_INSIGHT_OUTPUT_LABEL","aiInsightOutput","job","record","hostSubdomain","count","insights","length","resource","id","$id","versionId","hostId","label","note","aiInsightSurfaceForPath","pathname","segments","String","split","filter","Boolean","host","rest","slice","aiInsightSourceHref","table","where","path","source","orgSlug","scope","aiInsightCellText","key","undefined","column","columns","find","entry","kind","Intl","NumberFormat","style","currency","format","toLocaleString","maximumFractionDigits","seconds","Math","round","minutes","floor","padStart","aiInsightRowText","index","row","rows","map","join","aiInsightIsoWeek","at","date","Date","UTC","getUTCFullYear","getUTCMonth","getUTCDate","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime"],"mappings":"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"}
@@ -41,7 +41,29 @@ export interface AiJobPhaseSource {
41
41
  name: string;
42
42
  status: AiJobStepStatus;
43
43
  }>;
44
+ /** The wire summary's: the machine confirms this job's plan itself. */
45
+ autoConfirm?: boolean;
46
+ /** When the job last moved; the wire summary's ISO string. */
47
+ updatedAt?: unknown;
44
48
  }
49
+ /**
50
+ * How long a self-confirming plan may sit parked before it reads as waiting
51
+ * for a person after all: a confirmation that never came.
52
+ */
53
+ export declare const AI_JOB_SELF_CONFIRM_GRACE_MS: number;
54
+ /**
55
+ * Whether a job is parked on a plan the machine is about to confirm itself
56
+ * (AGL-3596). A guided site start records its plan step — parking the job on
57
+ * the plan as every planned job does — and confirms it in the next write, so
58
+ * a surface that reads in between sees `needs_review` for an instant. That
59
+ * instant is the build going on, never a stop: read as one, it showed the
60
+ * person a failed site that turned green again a moment later. The job's
61
+ * `updatedAt` there is the clock its step was claimed at, so the grace covers
62
+ * the plan step's own run as well as the confirmation. Past
63
+ * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is
64
+ * what it says.
65
+ */
66
+ export declare function aiJobConfirmingOwnPlan(job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>, now?: number): boolean;
45
67
  /**
46
68
  * Where one job stands, as a person reads it:
47
69
  *
@@ -14,11 +14,32 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { AI_JOB_PLAN_STEP } from "./ai-jobs.types.js";
17
+ /**
18
+ * How long a self-confirming plan may sit parked before it reads as waiting
19
+ * for a person after all: a confirmation that never came.
20
+ */ export const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60000;
21
+ /**
22
+ * Whether a job is parked on a plan the machine is about to confirm itself
23
+ * (AGL-3596). A guided site start records its plan step — parking the job on
24
+ * the plan as every planned job does — and confirms it in the next write, so
25
+ * a surface that reads in between sees `needs_review` for an instant. That
26
+ * instant is the build going on, never a stop: read as one, it showed the
27
+ * person a failed site that turned green again a moment later. The job's
28
+ * `updatedAt` there is the clock its step was claimed at, so the grace covers
29
+ * the plan step's own run as well as the confirmation. Past
30
+ * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is
31
+ * what it says.
32
+ */ export function aiJobConfirmingOwnPlan(job, now = Date.now()) {
33
+ var _job_review;
34
+ if (job.status !== 'needs_review' || ((_job_review = job.review) == null ? void 0 : _job_review.reason) !== 'plan' || job.autoConfirm !== true) return false;
35
+ const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN;
36
+ return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS;
37
+ }
17
38
  export function aiJobPhase(job) {
18
39
  if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {
19
40
  return job.status;
20
41
  }
21
- if (job.status === 'needs_review') {
42
+ if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {
22
43
  var _job_review;
23
44
  return ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' ? 'plan-ready' : 'attention';
24
45
  }
@@ -131,10 +152,12 @@ export function aiJobsActivity(jobs) {
131
152
  email: 'email',
132
153
  campaign: 'campaign',
133
154
  workflow: 'automation',
155
+ logic: 'function',
134
156
  theme: 'theme',
135
157
  products: 'products',
136
158
  experiment: 'experiment',
137
- insight: 'answer'
159
+ insight: 'answer',
160
+ build: 'build'
138
161
  };
139
162
  /** The noun for what a job of this kind makes, or `AI job` for a kind with none. */ export function aiJobKindNoun(kind) {
140
163
  var _KIND_NOUNS_kind;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review') {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n"],"names":["AI_JOB_PLAN_STEP","aiJobPhase","job","status","review","reason","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","theme","products","experiment","insight","aiJobKindNoun","kind"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","theme","products","experiment","insight","build","aiJobKindNoun","kind"],"mappings":"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"}
@@ -14,7 +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 { AiJobKind, AiJobStatus, AiJobSummary } from './ai-jobs.types';
17
+ import type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types';
18
18
  /**
19
19
  * What a member reads when a plan could not be made (AGL-3594): one plain
20
20
  * sentence and one thing to do, never a rule number or a validator's words.
@@ -86,3 +86,40 @@ export declare const AI_SITE_STARTER_FALLBACK_LABEL = "Use the starter site inst
86
86
  * is written only to a site with no page or layout.
87
87
  */
88
88
  export declare function aiSiteStarterFallbackOffered(job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>): boolean;
89
+ /**
90
+ * What a customer reads when an answer still broke a building rule after its
91
+ * re-ask (AGL-3596), in place of the doctrine's own sentence. "Rule 12
92
+ * (Responsive by the theme's breakpoints): Every child of a Grid container…"
93
+ * is what the re-ask is told and what staff read; shown to the person who
94
+ * asked for a site, it reads as the product breaking. The rule's words stay
95
+ * on the job (`review.detail`, an item's `failure.detail`) for staff.
96
+ */
97
+ export declare const AI_DOCTRINE_REFUSED_COPY: string;
98
+ /** The same, for a page. */
99
+ export declare const AI_DOCTRINE_REFUSED_PAGE_COPY: string;
100
+ /** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */
101
+ export declare function aiIsDoctrineRuleText(text: string | null | undefined): boolean;
102
+ /**
103
+ * A sentence a customer may read: the doctrine's own words become the plain
104
+ * refusal, a page's when the thing refused is a page; anything else stands.
105
+ */
106
+ export declare function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options?: {
107
+ page?: boolean;
108
+ }): T | string;
109
+ /**
110
+ * A doctrine review as a customer reads it (AGL-3596): a message in the
111
+ * doctrine's own words becomes the plain refusal, and those words move to
112
+ * `detail`, which only staff are shown. A review written before the step did
113
+ * this itself reads the same as one written after.
114
+ */
115
+ export declare function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & {
116
+ detail?: string;
117
+ }>(review: T, options?: {
118
+ page?: boolean;
119
+ }): T;
120
+ /**
121
+ * A build item's failure as a customer reads it (AGL-3596): a building rule
122
+ * still broken reads as the plain refusal, a page's for a page, and its
123
+ * checks' own sentence (`detail`) is left off.
124
+ */
125
+ export declare function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T;
@@ -1,3 +1,5 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
1
3
  /**
2
4
  * @license
3
5
  * Copyright 2026 Aglyn LLC
@@ -13,7 +15,8 @@
13
15
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
16
  * See the License for the specific language governing permissions and
15
17
  * limitations under the License.
16
- */ /**
18
+ */ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
19
+ /**
17
20
  * What a member reads when a plan could not be made (AGL-3594): one plain
18
21
  * sentence and one thing to do, never a rule number or a validator's words.
19
22
  *
@@ -101,5 +104,57 @@
101
104
  if (job.status === 'failed' || job.status === 'canceled') return true;
102
105
  return job.status === 'needs_review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'doctrine';
103
106
  }
107
+ /**
108
+ * What a customer reads when an answer still broke a building rule after its
109
+ * re-ask (AGL-3596), in place of the doctrine's own sentence. "Rule 12
110
+ * (Responsive by the theme's breakpoints): Every child of a Grid container…"
111
+ * is what the re-ask is told and what staff read; shown to the person who
112
+ * asked for a site, it reads as the product breaking. The rule's words stay
113
+ * on the job (`review.detail`, an item's `failure.detail`) for staff.
114
+ */ export const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`;
115
+ /** The same, for a page. */ export const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`;
116
+ /** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */ const AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.';
117
+ /** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */ export function aiIsDoctrineRuleText(text) {
118
+ if (!text) return false;
119
+ return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\bRule \d+ \(/.test(text);
120
+ }
121
+ /**
122
+ * A sentence a customer may read: the doctrine's own words become the plain
123
+ * refusal, a page's when the thing refused is a page; anything else stands.
124
+ */ export function aiCustomerSafeCopy(text, options = {}) {
125
+ if (!aiIsDoctrineRuleText(text)) return text;
126
+ return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY;
127
+ }
128
+ /**
129
+ * A doctrine review as a customer reads it (AGL-3596): a message in the
130
+ * doctrine's own words becomes the plain refusal, and those words move to
131
+ * `detail`, which only staff are shown. A review written before the step did
132
+ * this itself reads the same as one written after.
133
+ */ export function aiCustomerSafeReview(review, options = {}) {
134
+ var _review_detail;
135
+ if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review;
136
+ return _extends({}, review, {
137
+ message: aiCustomerSafeCopy(review.message, options),
138
+ detail: (_review_detail = review.detail) != null ? _review_detail : review.message
139
+ });
140
+ }
141
+ /**
142
+ * A build item's failure as a customer reads it (AGL-3596): a building rule
143
+ * still broken reads as the plain refusal, a page's for a page, and its
144
+ * checks' own sentence (`detail`) is left off.
145
+ */ export function aiCustomerSafeItem(row) {
146
+ const failure = row.failure;
147
+ if (!failure) return row;
148
+ const { detail: _detail } = failure, rest = _object_without_properties_loose(failure, [
149
+ "detail"
150
+ ]);
151
+ const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message);
152
+ if (!doctrine && _detail === undefined) return row;
153
+ return _extends({}, row, {
154
+ failure: _extends({}, rest, {
155
+ message: doctrine ? row.op === 'page' ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY : failure.message
156
+ })
157
+ });
158
+ }
104
159
 
105
160
  //# sourceMappingURL=ai-job-failure-copy.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 type { AiJobKind, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/** What a person's own cancel says about its credits: they paid for what ran until then. */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return AI_JOB_CANCELED_CREDITS_COPY\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n"],"names":["AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;CAgBC,GAED,0DAA0D,GAC1D,OAAO,MAAMA,yBAAyB,sBAAqB;AAE3D,qDAAqD,GACrD,SAASC,aAAaC,IAAwB;IAC5C,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAO;IACX;AACF;AAWA;;;;CAIC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;IAC7D,MAAMC,QAAQD,MAAME,QAAQ,GAAG,gDAAgD;IAC/E,IAAIF,MAAMG,KAAK,CAACC,QAAQ,CAACR,yBAAyB;QAChD,OAAO,CAAC,mDAAmD,EAAEK,MAAM,uCAAuC,CAAC;IAC7G;IACA,OAAO,CAAC,8BAA8B,EAAEJ,aAAaG,MAAMF,IAAI,IAAIG,MAAM,YAAY,CAAC;AACxF;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAmBL,KAAmD;IACpF,MAAMM,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACT,MAAMU,WAAW;IACrD,IAAIJ,QAAQN,MAAMW,WAAW,EAAE,OAAO;IACtC,OAAO,CAAC,SAAS,EAAEL,KAAK,IAAI,EAAEA,SAAS,IAAI,WAAW,UAAU,yCAAyC,EAAEN,MAAMW,WAAW,CAAC,uDAAuD,CAAC;AACvL;AAEA;;;;CAIC,GACD,OAAO,MAAMC,mCAAmC,iEAAgE;AAEhH,0FAA0F,GAC1F,OAAO,MAAMC,+BAA+B,0CAAyC;AAErF;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,GAA8F;QAEtDA,sBASNA;IATlC,MAAMb,WAAWK,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,EAACM,uBAAAA,IAAIC,eAAe,YAAnBD,uBAAuB;IAC/D,IAAIb,WAAW,GAAG;YAGqBa;QAFrC,MAAME,UAAU,GAAGf,SAAS,CAAC,EAAEA,aAAa,IAAI,WAAW,WAAW;QACtE,MAAMgB,OAAOhB,aAAa,IAAI,OAAO;QACrC,MAAMiB,QAAQjB,YAAYK,KAAKE,KAAK,EAACM,qBAAAA,IAAIK,YAAY,YAAhBL,qBAAoB;QACzD,OAAOI,QACH,CAAC,4CAA4C,EAAEF,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC,GACjG,CAAC,qEAAqE,EAAED,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC;IAChI;IACA,IAAIH,IAAIM,MAAM,KAAK,cAAc,EAACN,oBAAAA,IAAIK,YAAY,YAAhBL,oBAAoB,KAAK,GAAG,OAAOF;IACrE,OAAO;AACT;AAEA,2DAA2D,GAC3D,OAAO,MAAMS,iCAAiC,+BAA8B;AAE5E;;;;;;CAMC,GACD,OAAO,SAASC,6BACdR,GAA4E;QAMpCA;IAJxC,IAAIA,IAAIjB,IAAI,KAAK,UAAU,CAACiB,IAAIS,MAAM,EAAE,OAAO;IAC/C,2FAA2F;IAC3F,IAAIT,IAAIU,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,QAAQ,KAAK,QAAQ,OAAO;IACpE,IAAIb,IAAIM,MAAM,KAAK,YAAYN,IAAIM,MAAM,KAAK,YAAY,OAAO;IACjE,OAAON,IAAIM,MAAM,KAAK,kBAAkBN,EAAAA,cAAAA,IAAIc,MAAM,qBAAVd,YAAYe,MAAM,MAAK;AACjE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/** What a person's own cancel says about its credits: they paid for what ran until then. */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return AI_JOB_CANCELED_CREDITS_COPY\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n\n/**\n * What a customer reads when an answer still broke a building rule after its\n * re-ask (AGL-3596), in place of the doctrine's own sentence. \"Rule 12\n * (Responsive by the theme's breakpoints): Every child of a Grid container…\"\n * is what the re-ask is told and what staff read; shown to the person who\n * asked for a site, it reads as the product breaking. The rule's words stay\n * on the job (`review.detail`, an item's `failure.detail`) for staff.\n */\nexport const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`\n\n/** The same, for a page. */\nexport const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`\n\n/** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */\nconst AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.'\n\n/** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */\nexport function aiIsDoctrineRuleText(text: string | null | undefined): boolean {\n if (!text) return false\n return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\\bRule \\d+ \\(/.test(text)\n}\n\n/**\n * A sentence a customer may read: the doctrine's own words become the plain\n * refusal, a page's when the thing refused is a page; anything else stands.\n */\nexport function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options: { page?: boolean } = {}): T | string {\n if (!aiIsDoctrineRuleText(text)) return text\n return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY\n}\n\n/**\n * A doctrine review as a customer reads it (AGL-3596): a message in the\n * doctrine's own words becomes the plain refusal, and those words move to\n * `detail`, which only staff are shown. A review written before the step did\n * this itself reads the same as one written after.\n */\nexport function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & { detail?: string }>(\n review: T,\n options: { page?: boolean } = {},\n): T {\n if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review\n return { ...review, message: aiCustomerSafeCopy(review.message, options), detail: review.detail ?? review.message }\n}\n\n/**\n * A build item's failure as a customer reads it (AGL-3596): a building rule\n * still broken reads as the plain refusal, a page's for a page, and its\n * checks' own sentence (`detail`) is left off.\n */\nexport function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T {\n const failure = row.failure\n if (!failure) return row\n const { detail: _detail, ...rest } = failure\n const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message)\n if (!doctrine && _detail === undefined) return row\n return {\n ...row,\n failure: {\n ...rest,\n message: doctrine\n ? row.op === 'page'\n ? AI_DOCTRINE_REFUSED_PAGE_COPY\n : AI_DOCTRINE_REFUSED_COPY\n : failure.message,\n },\n }\n}\n"],"names":["PLATFORM_BRAND_NAME","AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason","AI_DOCTRINE_REFUSED_COPY","AI_DOCTRINE_REFUSED_PAGE_COPY","AI_DOCTRINE_REFUSAL_LEAD","aiIsDoctrineRuleText","text","startsWith","test","aiCustomerSafeCopy","options","page","aiCustomerSafeReview","message","detail","aiCustomerSafeItem","row","failure","_detail","rest","doctrine","undefined","op"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AAG3E;;;;;;;;;;;;;;;;CAgBC,GAED,0DAA0D,GAC1D,OAAO,MAAMC,yBAAyB,sBAAqB;AAE3D,qDAAqD,GACrD,SAASC,aAAaC,IAAwB;IAC5C,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAO;IACX;AACF;AAWA;;;;CAIC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;IAC7D,MAAMC,QAAQD,MAAME,QAAQ,GAAG,gDAAgD;IAC/E,IAAIF,MAAMG,KAAK,CAACC,QAAQ,CAACR,yBAAyB;QAChD,OAAO,CAAC,mDAAmD,EAAEK,MAAM,uCAAuC,CAAC;IAC7G;IACA,OAAO,CAAC,8BAA8B,EAAEJ,aAAaG,MAAMF,IAAI,IAAIG,MAAM,YAAY,CAAC;AACxF;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAmBL,KAAmD;IACpF,MAAMM,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACT,MAAMU,WAAW;IACrD,IAAIJ,QAAQN,MAAMW,WAAW,EAAE,OAAO;IACtC,OAAO,CAAC,SAAS,EAAEL,KAAK,IAAI,EAAEA,SAAS,IAAI,WAAW,UAAU,yCAAyC,EAAEN,MAAMW,WAAW,CAAC,uDAAuD,CAAC;AACvL;AAEA;;;;CAIC,GACD,OAAO,MAAMC,mCAAmC,iEAAgE;AAEhH,0FAA0F,GAC1F,OAAO,MAAMC,+BAA+B,0CAAyC;AAErF;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,GAA8F;QAEtDA,sBASNA;IATlC,MAAMb,WAAWK,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,EAACM,uBAAAA,IAAIC,eAAe,YAAnBD,uBAAuB;IAC/D,IAAIb,WAAW,GAAG;YAGqBa;QAFrC,MAAME,UAAU,GAAGf,SAAS,CAAC,EAAEA,aAAa,IAAI,WAAW,WAAW;QACtE,MAAMgB,OAAOhB,aAAa,IAAI,OAAO;QACrC,MAAMiB,QAAQjB,YAAYK,KAAKE,KAAK,EAACM,qBAAAA,IAAIK,YAAY,YAAhBL,qBAAoB;QACzD,OAAOI,QACH,CAAC,4CAA4C,EAAEF,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC,GACjG,CAAC,qEAAqE,EAAED,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC;IAChI;IACA,IAAIH,IAAIM,MAAM,KAAK,cAAc,EAACN,oBAAAA,IAAIK,YAAY,YAAhBL,oBAAoB,KAAK,GAAG,OAAOF;IACrE,OAAO;AACT;AAEA,2DAA2D,GAC3D,OAAO,MAAMS,iCAAiC,+BAA8B;AAE5E;;;;;;CAMC,GACD,OAAO,SAASC,6BACdR,GAA4E;QAMpCA;IAJxC,IAAIA,IAAIjB,IAAI,KAAK,UAAU,CAACiB,IAAIS,MAAM,EAAE,OAAO;IAC/C,2FAA2F;IAC3F,IAAIT,IAAIU,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,QAAQ,KAAK,QAAQ,OAAO;IACpE,IAAIb,IAAIM,MAAM,KAAK,YAAYN,IAAIM,MAAM,KAAK,YAAY,OAAO;IACjE,OAAON,IAAIM,MAAM,KAAK,kBAAkBN,EAAAA,cAAAA,IAAIc,MAAM,qBAAVd,YAAYe,MAAM,MAAK;AACjE;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA2B,GAAGpC,oBAAoB,qFAAqF,CAAC,CAAA;AAErJ,0BAA0B,GAC1B,OAAO,MAAMqC,gCAAgC,GAAGrC,oBAAoB,wFAAwF,CAAC,CAAA;AAE7J,gFAAgF,GAChF,MAAMsC,2BAA2B;AAEjC,4FAA4F,GAC5F,OAAO,SAASC,qBAAqBC,IAA+B;IAClE,IAAI,CAACA,MAAM,OAAO;IAClB,OAAOA,KAAKC,UAAU,CAACH,6BAA6B,gBAAgBI,IAAI,CAACF;AAC3E;AAEA;;;CAGC,GACD,OAAO,SAASG,mBAAwDH,IAAO,EAAEI,UAA8B,CAAC,CAAC;IAC/G,IAAI,CAACL,qBAAqBC,OAAO,OAAOA;IACxC,OAAOI,QAAQC,IAAI,GAAGR,gCAAgCD;AACxD;AAEA;;;;;CAKC,GACD,OAAO,SAASU,qBACdZ,MAAS,EACTU,UAA8B,CAAC,CAAC;QAGkDV;IADlF,IAAIA,OAAOC,MAAM,KAAK,cAAc,CAACI,qBAAqBL,OAAOa,OAAO,GAAG,OAAOb;IAClF,OAAO,aAAKA;QAAQa,SAASJ,mBAAmBT,OAAOa,OAAO,EAAEH;QAAUI,MAAM,GAAEd,iBAAAA,OAAOc,MAAM,YAAbd,iBAAiBA,OAAOa,OAAO;;AACnH;AAEA;;;;CAIC,GACD,OAAO,SAASE,mBAAsEC,GAAM;IAC1F,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,IAAI,CAACA,SAAS,OAAOD;IACrB,MAAM,EAAEF,QAAQI,OAAO,EAAW,GAAGD,SAATE,wCAASF;;;IACrC,MAAMG,WAAWH,QAAQhB,MAAM,KAAK,sBAAsBI,qBAAqBY,QAAQJ,OAAO;IAC9F,IAAI,CAACO,YAAYF,YAAYG,WAAW,OAAOL;IAC/C,OAAO,aACFA;QACHC,SAAS,aACJE;YACHN,SAASO,WACLJ,IAAIM,EAAE,KAAK,SACTnB,gCACAD,2BACFe,QAAQJ,OAAO;;;AAGzB"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { AglynNotification } from '@aglyn/aglyn/app-utils/notifications';
18
- import type { AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types';
18
+ import type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types';
19
19
  /** The besigner segment each versioned resource lives under. */
20
20
  export declare const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>>;
21
21
  /**
@@ -44,6 +44,8 @@ export interface AiJobNoticeSource {
44
44
  /** What the job spent, and gave back for a failure on our side (AGL-3596). */
45
45
  creditsSpent?: number;
46
46
  refundedCredits?: number;
47
+ /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */
48
+ items?: AiJobItemLedger[] | null;
47
49
  }
48
50
  export type AiJobNotice = Pick<AglynNotification, 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'>;
49
51
  /**