@aglyn/plugins-ai 1.0.0-beta.270 → 1.0.0-beta.272

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 (281) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +4 -3
  3. package/src/lib/activity/ai-activity-actions.js +4 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/assets/site-styles/beauty.webp +0 -0
  6. package/src/lib/assets/site-styles/blog.webp +0 -0
  7. package/src/lib/assets/site-styles/business.webp +0 -0
  8. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  9. package/src/lib/assets/site-styles/education.webp +0 -0
  10. package/src/lib/assets/site-styles/events.webp +0 -0
  11. package/src/lib/assets/site-styles/fitness.webp +0 -0
  12. package/src/lib/assets/site-styles/landing.webp +0 -0
  13. package/src/lib/assets/site-styles/music.webp +0 -0
  14. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  15. package/src/lib/assets/site-styles/personal.webp +0 -0
  16. package/src/lib/assets/site-styles/photography.webp +0 -0
  17. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  18. package/src/lib/assets/site-styles/professional.webp +0 -0
  19. package/src/lib/assets/site-styles/realestate.webp +0 -0
  20. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  21. package/src/lib/assets/site-styles/store.webp +0 -0
  22. package/src/lib/assets/site-styles/studio.webp +0 -0
  23. package/src/lib/assets/site-styles/trades.webp +0 -0
  24. package/src/lib/assets/site-styles/wellness.webp +0 -0
  25. package/src/lib/assets/site-styles/yoga.webp +0 -0
  26. package/src/lib/components/ai-credits-card.component.d.ts +4 -3
  27. package/src/lib/components/ai-credits-card.component.js +15 -15
  28. package/src/lib/components/ai-credits-card.component.js.map +1 -1
  29. package/src/lib/components/ai-job-input-form.component.d.ts +36 -0
  30. package/src/lib/components/ai-job-input-form.component.js +100 -0
  31. package/src/lib/components/ai-job-input-form.component.js.map +1 -0
  32. package/src/lib/components/ai-job-requests.d.ts +4 -0
  33. package/src/lib/components/ai-job-requests.js +4 -0
  34. package/src/lib/components/ai-job-requests.js.map +1 -1
  35. package/src/lib/components/ai-site-build-page.component.js +18 -0
  36. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  37. package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
  38. package/src/lib/components/ai-site-import-job.component.js +707 -0
  39. package/src/lib/components/ai-site-import-job.component.js.map +1 -0
  40. package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
  41. package/src/lib/components/ai-site-import-start.component.js +335 -0
  42. package/src/lib/components/ai-site-import-start.component.js.map +1 -0
  43. package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
  44. package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
  45. package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
  46. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  47. package/src/lib/components/ai-site-start-card.component.js +261 -119
  48. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  49. package/src/lib/components/ai-site-start-steps.component.d.ts +43 -2
  50. package/src/lib/components/ai-site-start-steps.component.js +651 -15
  51. package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
  52. package/src/lib/components/ai-site-start-tracking.d.ts +10 -0
  53. package/src/lib/components/ai-site-start-tracking.js +14 -0
  54. package/src/lib/components/ai-site-start-tracking.js.map +1 -1
  55. package/src/lib/components/ai-site-style-previews.js +26 -22
  56. package/src/lib/components/ai-site-style-previews.js.map +1 -1
  57. package/src/lib/components/ai-upsell-dialog.component.d.ts +7 -5
  58. package/src/lib/components/ai-upsell-dialog.component.js +8 -6
  59. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  60. package/src/lib/components/assist-build-card.component.js +11 -1
  61. package/src/lib/components/assist-build-card.component.js.map +1 -1
  62. package/src/lib/components/assist-jobs-drawer.component.js +9 -1
  63. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  64. package/src/lib/components/assist-panel.component.js +8 -4
  65. package/src/lib/components/assist-panel.component.js.map +1 -1
  66. package/src/lib/components/staff-org-ai-card.component.js +4 -7
  67. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  68. package/src/lib/declarations.d.ts +2 -2
  69. package/src/lib/declarations.js +9 -5
  70. package/src/lib/declarations.js.map +1 -1
  71. package/src/lib/jobs/ai-build-capabilities.js +3 -1
  72. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  73. package/src/lib/jobs/ai-business-profile-prefill.d.ts +1 -1
  74. package/src/lib/jobs/ai-business-profile-prefill.js +14 -3
  75. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
  76. package/src/lib/jobs/ai-job-build-step.js +5 -3
  77. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  78. package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
  79. package/src/lib/jobs/ai-job-drafts.js +24 -0
  80. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  81. package/src/lib/jobs/ai-job-form-step.d.ts +15 -1
  82. package/src/lib/jobs/ai-job-form-step.js +75 -8
  83. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  84. package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
  85. package/src/lib/jobs/ai-job-import-step.js +809 -0
  86. package/src/lib/jobs/ai-job-import-step.js.map +1 -0
  87. package/src/lib/jobs/ai-job-page-language.d.ts +52 -1
  88. package/src/lib/jobs/ai-job-page-language.js +99 -5
  89. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  90. package/src/lib/jobs/ai-job-page-step.d.ts +19 -2
  91. package/src/lib/jobs/ai-job-page-step.js +42 -18
  92. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  93. package/src/lib/jobs/ai-job-plan-step.d.ts +25 -2
  94. package/src/lib/jobs/ai-job-plan-step.js +103 -29
  95. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  96. package/src/lib/jobs/ai-job-settings-step.js +4 -3
  97. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  98. package/src/lib/jobs/ai-job-site-datasets.d.ts +27 -0
  99. package/src/lib/jobs/ai-job-site-datasets.js +140 -14
  100. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  101. package/src/lib/jobs/ai-job-site-look.js +18 -5
  102. package/src/lib/jobs/ai-job-site-look.js.map +1 -1
  103. package/src/lib/jobs/ai-job-site-step.d.ts +16 -1
  104. package/src/lib/jobs/ai-job-site-step.js +95 -22
  105. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-text-step.d.ts +15 -1
  107. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  108. package/src/lib/jobs/ai-job-tracking-step.d.ts +14 -3
  109. package/src/lib/jobs/ai-job-tracking-step.js +28 -5
  110. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  111. package/src/lib/jobs/ai-jobs.d.ts +13 -1
  112. package/src/lib/jobs/ai-jobs.js +77 -10
  113. package/src/lib/jobs/ai-jobs.js.map +1 -1
  114. package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
  115. package/src/lib/jobs/ai-layout-owner-photos.js +47 -0
  116. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  117. package/src/lib/jobs/ai-layout-stock-photos.d.ts +27 -2
  118. package/src/lib/jobs/ai-layout-stock-photos.js +190 -19
  119. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  120. package/src/lib/jobs/ai-site-inspiration-signals.d.ts +98 -0
  121. package/src/lib/jobs/ai-site-inspiration-signals.js +585 -0
  122. package/src/lib/jobs/ai-site-inspiration-signals.js.map +1 -0
  123. package/src/lib/jobs/ai-site-inspiration.d.ts +157 -0
  124. package/src/lib/jobs/ai-site-inspiration.js +346 -0
  125. package/src/lib/jobs/ai-site-inspiration.js.map +1 -0
  126. package/src/lib/jobs/ai-site-publish.d.ts +26 -1
  127. package/src/lib/jobs/ai-site-publish.js +162 -13
  128. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  129. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +77 -3
  130. package/src/lib/jobs/ai-theme-brand-inputs.js +135 -31
  131. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  132. package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.d.ts +97 -0
  133. package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js +137 -0
  134. package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js.map +1 -0
  135. package/src/lib/jobs/fixtures/ai-site-inspiration-pages.d.ts +38 -0
  136. package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js +103 -0
  137. package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js.map +1 -0
  138. package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.d.ts +25 -0
  139. package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js +174 -0
  140. package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js.map +1 -0
  141. package/src/lib/layout-language/ai-layout-compiler.d.ts +1 -1
  142. package/src/lib/layout-language/ai-layout-compiler.js +438 -25
  143. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  144. package/src/lib/layout-language/ai-layout-contact.d.ts +57 -0
  145. package/src/lib/layout-language/ai-layout-contact.js +137 -0
  146. package/src/lib/layout-language/ai-layout-contact.js.map +1 -0
  147. package/src/lib/layout-language/ai-layout-copy.d.ts +12 -0
  148. package/src/lib/layout-language/ai-layout-copy.js +34 -2
  149. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  150. package/src/lib/layout-language/ai-layout-design.d.ts +65 -8
  151. package/src/lib/layout-language/ai-layout-design.js +411 -97
  152. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  153. package/src/lib/layout-language/ai-layout-frame.js +23 -0
  154. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  155. package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
  156. package/src/lib/layout-language/ai-layout-labels.js +12 -6
  157. package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
  158. package/src/lib/layout-language/ai-layout-language.d.ts +3 -2
  159. package/src/lib/layout-language/ai-layout-language.js +4 -3
  160. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  161. package/src/lib/layout-language/ai-layout-links.d.ts +13 -0
  162. package/src/lib/layout-language/ai-layout-links.js +30 -0
  163. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  164. package/src/lib/layout-language/ai-layout-pictures.d.ts +49 -6
  165. package/src/lib/layout-language/ai-layout-pictures.js +145 -22
  166. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  167. package/src/lib/model/ai-build-job.d.ts +16 -1
  168. package/src/lib/model/ai-build-job.js +40 -0
  169. package/src/lib/model/ai-build-job.js.map +1 -1
  170. package/src/lib/model/ai-credit-estimate.d.ts +20 -0
  171. package/src/lib/model/ai-credit-estimate.js +13 -0
  172. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  173. package/src/lib/model/ai-job-activity.d.ts +2 -2
  174. package/src/lib/model/ai-job-activity.js +6 -4
  175. package/src/lib/model/ai-job-activity.js.map +1 -1
  176. package/src/lib/model/ai-job-input.d.ts +33 -0
  177. package/src/lib/model/ai-job-input.js +59 -0
  178. package/src/lib/model/ai-job-input.js.map +1 -0
  179. package/src/lib/model/ai-job-notice.d.ts +4 -0
  180. package/src/lib/model/ai-job-notice.js +28 -4
  181. package/src/lib/model/ai-job-notice.js.map +1 -1
  182. package/src/lib/model/ai-jobs.types.d.ts +69 -3
  183. package/src/lib/model/ai-jobs.types.js +2 -1
  184. package/src/lib/model/ai-jobs.types.js.map +1 -1
  185. package/src/lib/model/ai-site-details.d.ts +122 -0
  186. package/src/lib/model/ai-site-details.js +300 -0
  187. package/src/lib/model/ai-site-details.js.map +1 -0
  188. package/src/lib/model/ai-site-entity.d.ts +181 -0
  189. package/src/lib/model/ai-site-entity.js +390 -0
  190. package/src/lib/model/ai-site-entity.js.map +1 -0
  191. package/src/lib/model/ai-site-import-view.d.ts +130 -0
  192. package/src/lib/model/ai-site-import-view.js +182 -0
  193. package/src/lib/model/ai-site-import-view.js.map +1 -0
  194. package/src/lib/model/ai-site-import.d.ts +302 -0
  195. package/src/lib/model/ai-site-import.js +526 -0
  196. package/src/lib/model/ai-site-import.js.map +1 -0
  197. package/src/lib/model/ai-site-inspiration.d.ts +72 -0
  198. package/src/lib/model/ai-site-inspiration.js +109 -0
  199. package/src/lib/model/ai-site-inspiration.js.map +1 -0
  200. package/src/lib/model/ai-site-job.d.ts +13 -1
  201. package/src/lib/model/ai-site-job.js +15 -2
  202. package/src/lib/model/ai-site-job.js.map +1 -1
  203. package/src/lib/model/ai-site-kinds.js +109 -21
  204. package/src/lib/model/ai-site-kinds.js.map +1 -1
  205. package/src/lib/model/ai-site-look.d.ts +10 -0
  206. package/src/lib/model/ai-site-look.js +24 -0
  207. package/src/lib/model/ai-site-look.js.map +1 -1
  208. package/src/lib/model/ai-site-start-seo.d.ts +19 -5
  209. package/src/lib/model/ai-site-start-seo.js +25 -7
  210. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  211. package/src/lib/model/ai-site-start-steps.d.ts +9 -3
  212. package/src/lib/model/ai-site-start-steps.js +67 -5
  213. package/src/lib/model/ai-site-start-steps.js.map +1 -1
  214. package/src/lib/model/ai-site-start.d.ts +81 -4
  215. package/src/lib/model/ai-site-start.js +207 -13
  216. package/src/lib/model/ai-site-start.js.map +1 -1
  217. package/src/lib/model/ai-upsell-copy.js +4 -1
  218. package/src/lib/model/ai-upsell-copy.js.map +1 -1
  219. package/src/lib/plan-entitlements.d.ts +4 -3
  220. package/src/lib/plan-entitlements.js +3 -8
  221. package/src/lib/plan-entitlements.js.map +1 -1
  222. package/src/lib/runtime/ai-dataset-generation.d.ts +8 -1
  223. package/src/lib/runtime/ai-dataset-generation.js +12 -2
  224. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  225. package/src/lib/runtime/ai-doctrine-validators.d.ts +26 -33
  226. package/src/lib/runtime/ai-doctrine-validators.js +54 -44
  227. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  228. package/src/lib/runtime/ai-doctrine.js +1 -1
  229. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  230. package/src/lib/runtime/ai-node-tree.d.ts +7 -0
  231. package/src/lib/runtime/ai-node-tree.js +9 -1
  232. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  233. package/src/lib/server/ai-create-route.d.ts +1 -1
  234. package/src/lib/server/ai-create-route.js +3 -4
  235. package/src/lib/server/ai-create-route.js.map +1 -1
  236. package/src/lib/server/ai-jobs-resume.js +27 -3
  237. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  238. package/src/lib/server/ai-jobs-route.js +18 -0
  239. package/src/lib/server/ai-jobs-route.js.map +1 -1
  240. package/src/lib/server/ai-site-import-route.d.ts +89 -0
  241. package/src/lib/server/ai-site-import-route.js +356 -0
  242. package/src/lib/server/ai-site-import-route.js.map +1 -0
  243. package/src/lib/server.js +14 -0
  244. package/src/lib/server.js.map +1 -1
  245. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
  246. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
  247. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
  248. package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
  249. package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
  250. package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
  251. package/src/lib/site-import/site-import-crawl.d.ts +163 -0
  252. package/src/lib/site-import/site-import-crawl.js +467 -0
  253. package/src/lib/site-import/site-import-crawl.js.map +1 -0
  254. package/src/lib/site-import/site-import-flag.d.ts +23 -0
  255. package/src/lib/site-import/site-import-flag.js +23 -0
  256. package/src/lib/site-import/site-import-flag.js.map +1 -0
  257. package/src/lib/site-import/site-import-html.d.ts +94 -0
  258. package/src/lib/site-import/site-import-html.js +462 -0
  259. package/src/lib/site-import/site-import-html.js.map +1 -0
  260. package/src/lib/site-import/site-import-images.d.ts +54 -0
  261. package/src/lib/site-import/site-import-images.js +204 -0
  262. package/src/lib/site-import/site-import-images.js.map +1 -0
  263. package/src/lib/site-import/site-import-page.d.ts +48 -0
  264. package/src/lib/site-import/site-import-page.js +150 -0
  265. package/src/lib/site-import/site-import-page.js.map +1 -0
  266. package/src/lib/site-import/site-import-review.d.ts +40 -0
  267. package/src/lib/site-import/site-import-review.js +149 -0
  268. package/src/lib/site-import/site-import-review.js.map +1 -0
  269. package/src/lib/site-import/site-import-robots.d.ts +52 -0
  270. package/src/lib/site-import/site-import-robots.js +126 -0
  271. package/src/lib/site-import/site-import-robots.js.map +1 -0
  272. package/src/lib/usage/assist-credits.d.ts +4 -4
  273. package/src/lib/usage/assist-credits.js +4 -4
  274. package/src/lib/usage/assist-credits.js.map +1 -1
  275. package/src/lib/usage/staff-org-ai-view.js +6 -3
  276. package/src/lib/usage/staff-org-ai-view.js.map +1 -1
  277. package/src/lib/usage/staff-org-ai.d.ts +4 -2
  278. package/src/lib/usage/staff-org-ai.js +7 -7
  279. package/src/lib/usage/staff-org-ai.js.map +1 -1
  280. package/src/lib/usage/staff-org-usage-ai.js +2 -2
  281. package/src/lib/usage/staff-org-usage-ai.js.map +1 -1
@@ -13,7 +13,11 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { aiSiteKind, aiSiteKindFor } from "./ai-site-kinds.js";
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { weeklyHoursFromText, weeklyHoursToText } from "@aglyn/shared-ui-jsx/utils/weekly-hours";
18
+ import { AI_SITE_DETAIL_KEYS, AI_SITE_NO_DETAILS, aiSiteDetailProblem, aiSiteDetailsOf } from "./ai-site-details.js";
19
+ import { aiSiteKind, aiSiteKindFor } from "./ai-site-kinds.js";
20
+ import { AI_SITE_INSPIRATION_INPUT, aiSiteInspirationInput, aiSiteInspirationLimit, aiSiteInspirationRefusal, aiSiteInspirationUrl } from "./ai-site-inspiration.js";
17
21
  import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from "./ai-site-job.js";
18
22
  /**
19
23
  * The examples the start offers, in the order they are shown. The platform's
@@ -71,15 +75,49 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
71
75
  * unless it is told not to, and the Inbox because it is what every submission
72
76
  * does anyway — filing a lead as well is the addition, and an addition is the
73
77
  * thing a person should have to choose.
74
- */ export const AI_SITE_START_ANSWERS = {
78
+ */ export const AI_SITE_START_ANSWERS = _extends({
75
79
  siteType: '',
76
80
  audience: '',
77
81
  example: null,
78
82
  kind: null,
79
83
  pages: 5,
80
84
  submissions: 'inbox',
81
- welcomeEmail: true
82
- };
85
+ welcomeEmail: true,
86
+ city: '',
87
+ brand: ''
88
+ }, AI_SITE_NO_DETAILS);
89
+ /** Every extra, in the order the review lists them. */ export const AI_SITE_START_EXTRA_KEYS = [
90
+ 'phone',
91
+ 'email',
92
+ 'tagline',
93
+ 'founded',
94
+ 'serviceArea',
95
+ 'priceRange',
96
+ 'languages',
97
+ 'brand',
98
+ 'street',
99
+ 'city',
100
+ 'region',
101
+ 'postalCode',
102
+ 'country',
103
+ 'hours',
104
+ 'social'
105
+ ];
106
+ /** What is wrong with one extra, worded for beside its field; `null` when it may be used. */ export function aiSiteStartExtraProblem(key, value) {
107
+ if (key === 'city' || key === 'brand') {
108
+ return value.trim().length > AI_SITE_INPUT_MAX_CHARS ? {
109
+ code: 'too_long',
110
+ message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.`
111
+ } : null;
112
+ }
113
+ return aiSiteDetailProblem(key, value);
114
+ }
115
+ /** The extras a person gave, in the review's order: what "More details" was used for. */ export function aiSiteStartExtrasGiven(answers) {
116
+ return AI_SITE_START_EXTRA_KEYS.filter((key)=>{
117
+ var _answers_key;
118
+ return ((_answers_key = answers[key]) != null ? _answers_key : '').trim() !== '';
119
+ });
120
+ }
83
121
  /** The kind of site the answers pick, else the one what the site is and who it is for suggest. */ export function aiSiteStartKind(answers) {
84
122
  var _aiSiteKind, _answers_audience;
85
123
  return (_aiSiteKind = aiSiteKind(answers.kind)) != null ? _aiSiteKind : aiSiteKindFor(answers.siteType, (_answers_audience = answers.audience) != null ? _answers_audience : '');
@@ -113,7 +151,23 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
113
151
  if (!AI_SITE_SUBMISSIONS.includes(answers.submissions)) {
114
152
  return 'Say where the contact form’s submissions go.';
115
153
  }
116
- return null;
154
+ for (const key of AI_SITE_START_EXTRA_KEYS){
155
+ var _answers_key;
156
+ const problem = aiSiteStartExtraProblem(key, (_answers_key = answers[key]) != null ? _answers_key : '');
157
+ if (problem) return problem.message;
158
+ }
159
+ return aiSiteStartInspirationRefusal(answers, options);
160
+ }
161
+ /** The sites they typed, without the empty fields. */ export function aiSiteStartInspirationTyped(answers) {
162
+ var _answers_inspiration;
163
+ return ((_answers_inspiration = answers.inspiration) != null ? _answers_inspiration : []).map((entry)=>entry.trim()).filter(Boolean);
164
+ }
165
+ /** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */ export function aiSiteStartInspirationRefusal(answers, options = {}) {
166
+ const typed = aiSiteStartInspirationTyped(answers);
167
+ if (typed.some((entry)=>!aiSiteInspirationUrl(entry))) {
168
+ return 'Give each site you like as its https address, like https://example.com.';
169
+ }
170
+ return aiSiteInspirationRefusal(typed, options.freeTaste);
117
171
  }
118
172
  /**
119
173
  * The answers as the brief the `site` job is created with: the plan step reads
@@ -157,21 +211,35 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
157
211
  * reached the model with no name had the model invent one for its header and
158
212
  * footer.
159
213
  */ export function aiSiteStartInputs(answers) {
160
- var _ref;
214
+ var _ref, _answers_city, _answers_brand;
161
215
  const example = aiSiteStartExample(answers);
162
216
  const kind = aiSiteStartKind(answers);
163
- return {
217
+ // The details are sent only where given (AGL-3979): the plan step lists
218
+ // every scalar input on a line of its own, and a start without them is
219
+ // asked exactly what it was asked before.
220
+ const details = {};
221
+ for (const key of AI_SITE_DETAIL_KEYS){
222
+ var _answers_key;
223
+ const value = ((_answers_key = answers[key]) != null ? _answers_key : '').trim();
224
+ if (value) details[key] = value;
225
+ }
226
+ // The sites they like travel only when there are some (AGL-3975), so a
227
+ // start without them asks for exactly what it always did.
228
+ const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers));
229
+ return _extends({
164
230
  businessType: answers.siteType.trim(),
165
231
  audience: answers.audience.trim(),
166
232
  starter: (_ref = example == null ? void 0 : example.id) != null ? _ref : kind.starter,
167
233
  siteKind: kind.id,
168
234
  pages: answers.pages,
169
235
  businessName: '',
170
- city: '',
171
- brand: '',
236
+ city: ((_answers_city = answers.city) != null ? _answers_city : '').trim(),
237
+ brand: ((_answers_brand = answers.brand) != null ? _answers_brand : '').trim(),
172
238
  submissions: answers.submissions,
173
239
  welcomeEmail: answers.welcomeEmail
174
- };
240
+ }, details, inspiration ? {
241
+ [AI_SITE_INSPIRATION_INPUT]: inspiration
242
+ } : {});
175
243
  }
176
244
  /** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */ export const AI_SITE_START_INPUT_KEYS = [
177
245
  'businessType',
@@ -183,7 +251,9 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
183
251
  'city',
184
252
  'brand',
185
253
  'submissions',
186
- 'welcomeEmail'
254
+ 'welcomeEmail',
255
+ ...AI_SITE_DETAIL_KEYS,
256
+ AI_SITE_INSPIRATION_INPUT
187
257
  ];
188
258
  /** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */ export function aiSiteStartInputsOf(inputs) {
189
259
  if (!inputs) return null;
@@ -210,15 +280,139 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
210
280
  const pages = typeof (inputs == null ? void 0 : inputs['pages']) === 'number' ? Math.round(inputs['pages']) : AI_SITE_START_ANSWERS.pages;
211
281
  const starter = text('starter');
212
282
  const submissions = text('submissions');
213
- return {
283
+ return _extends({
214
284
  siteType,
215
285
  audience: text('audience'),
216
286
  example: AI_SITE_START_EXAMPLES.some((entry)=>entry.id === starter) ? starter : null,
217
287
  kind: (_ref = (_aiSiteKind = aiSiteKind(text('siteKind'))) == null ? void 0 : _aiSiteKind.id) != null ? _ref : null,
218
288
  pages: Math.min(band.max, Math.max(band.min, pages)),
219
289
  submissions: AI_SITE_SUBMISSIONS.includes(submissions) ? submissions : AI_SITE_START_ANSWERS.submissions,
220
- welcomeEmail: options.freeTaste ? false : (inputs == null ? void 0 : inputs['welcomeEmail']) !== false
290
+ welcomeEmail: options.freeTaste ? false : (inputs == null ? void 0 : inputs['welcomeEmail']) !== false,
291
+ city: text('city'),
292
+ brand: text('brand')
293
+ }, Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key)=>[
294
+ key,
295
+ text(key)
296
+ ])), text(AI_SITE_INSPIRATION_INPUT) ? {
297
+ inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste))
298
+ } : {});
299
+ }
300
+ // ── A build staff run for a client (AGL-3958) ───────────────────────────────
301
+ /** Shortens an answer to what a scalar input admits, on a word where it can. */ function bounded(value, max) {
302
+ const trimmed = value.trim().replace(/\s+/g, ' ');
303
+ if (trimmed.length <= max) return trimmed;
304
+ const cut = trimmed.slice(0, max);
305
+ const space = cut.lastIndexOf(' ');
306
+ return (space > max / 2 ? cut.slice(0, space) : cut).trim();
307
+ }
308
+ /** A time with am or pm: "9am", "5:30 pm", "12 a.m.". */ const TWELVE_HOUR_TIME = /\b(\d{1,2})(?::(\d{2}))?\s*([ap])\.?m\.?(?![a-z])/gi;
309
+ /**
310
+ * The hours an owner gave, in the shape the Details step's hours picker
311
+ * writes (`Tu-Sa 09:00-17:00`, one set of days a line), so the plan reads
312
+ * them as it reads hours picked there. A time with am or pm is the time it
313
+ * names, "to" joins a range as a dash does, and days with their hours may be
314
+ * set apart by a comma, a semicolon or a line. Whatever still does not read
315
+ * as hours — "8–5", "Sat by appointment" — is left out rather than guessed at.
316
+ */ function clientBuildHours(hours) {
317
+ const lines = hours.replace(TWELVE_HOUR_TIME, (time, hour, minutes, half)=>{
318
+ const h = Number(hour);
319
+ if (h < 1 || h > 12) return time;
320
+ const clock = h % 12 + (half.toLowerCase() === 'p' ? 12 : 0);
321
+ return `${String(clock).padStart(2, '0')}:${minutes != null ? minutes : '00'}`;
322
+ }).replace(/\s+to\s+/gi, '-').replace(/(\d{1,2}:\d{2})\s*,\s*/g, '$1\n').replace(/;/g, '\n');
323
+ const week = weeklyHoursFromText(lines).hours;
324
+ return week.some((day)=>day.length) ? weeklyHoursToText(week) : '';
325
+ }
326
+ /**
327
+ * The towns an intake serves, one a line, as the Details step's "Areas you
328
+ * serve" holds them: "Austin, Round Rock and Pflugerville" is three.
329
+ */ function clientBuildServiceArea(towns) {
330
+ return towns.split(/\r?\n|[,;]|\s+(?:and|&)\s+/i).map((town)=>town.trim()).filter(Boolean).join('\n');
331
+ }
332
+ /**
333
+ * The questions' answers for a site staff are building for a client: what
334
+ * the business does is the kind of site, and the details the intake holds
335
+ * fill the Details step's own fields (AGL-3979, AGL-3995): the phone, the
336
+ * email, the towns as the areas it serves and the hours, each held to that
337
+ * detail's own rule, so one the Details step would refuse is left empty
338
+ * there. Every other answer keeps its default, which the review shows to
339
+ * change. The intake, a refused detail included, also reaches the job
340
+ * through {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.
341
+ */ export function aiSiteStartAnswersFromClientBuild(intake) {
342
+ const details = aiSiteDetailsOf({
343
+ phone: intake.phone,
344
+ email: intake.email,
345
+ serviceArea: clientBuildServiceArea(intake.towns),
346
+ hours: clientBuildHours(intake.hours)
347
+ });
348
+ return _extends({}, AI_SITE_START_ANSWERS, {
349
+ siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS),
350
+ phone: details.phone,
351
+ email: details.email,
352
+ serviceArea: details.serviceArea,
353
+ hours: details.hours
354
+ });
355
+ }
356
+ /**
357
+ * The per-site inputs the intake fills, which the guided start's own
358
+ * questions leave empty: the business's name for the header and footer, and
359
+ * the towns it serves as its place.
360
+ *
361
+ * The towns are the place only while no address is given. The Entity reads
362
+ * `city` as the address's own locality wherever there is a street or a
363
+ * postal code (`aiSiteEntity`), so once the answers carry either, `city` is
364
+ * the city the Details step was given with that address, never the towns
365
+ * (AGL-3995). The intake asks no address, so today that is the Details step's
366
+ * own, typed on the review; the towns still reach the site as the areas it
367
+ * serves.
368
+ */ export function aiSiteStartClientInputs(intake, answers) {
369
+ var _ref;
370
+ const addressed = Boolean((answers == null ? void 0 : answers.street.trim()) || (answers == null ? void 0 : answers.postalCode.trim()));
371
+ return {
372
+ businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),
373
+ city: bounded(addressed ? (_ref = answers == null ? void 0 : answers.city) != null ? _ref : '' : intake.towns, AI_SITE_INPUT_MAX_CHARS)
221
374
  };
222
375
  }
376
+ /**
377
+ * The intake as sentences added to the brief: how to reach the business and
378
+ * when it is open, which the site should say where a visitor looks for them,
379
+ * and the owner's own words, quoted, which the copy should sound like. Empty
380
+ * answers say nothing.
381
+ */ export function aiSiteStartClientBrief(intake) {
382
+ const name = intake.businessName.trim();
383
+ const towns = intake.towns.trim();
384
+ const phone = intake.phone.trim();
385
+ const email = intake.email.trim();
386
+ const hours = intake.hours.trim();
387
+ const words = intake.ownerWords.trim().replace(/\s+/g, ' ');
388
+ const sentences = [];
389
+ if (name) sentences.push(`The business is called ${name}.`);
390
+ if (towns) sentences.push(`It serves ${towns}.`);
391
+ const contact = [
392
+ phone ? `call ${phone}` : '',
393
+ email ? `email ${email}` : ''
394
+ ].filter(Boolean);
395
+ if (contact.length) {
396
+ sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`);
397
+ }
398
+ if (hours) sentences.push(`Its hours are ${hours}.`);
399
+ if (words) sentences.push(`In the owner's own words: "${words}" Write the copy so it sounds like them.`);
400
+ return sentences.join(' ');
401
+ }
402
+ /**
403
+ * What the review says the client build adds beyond its own rows, so staff
404
+ * see the intake is used without it being asked again: the business's name
405
+ * and the owner's own words, which no row shows. The phone, email, towns and
406
+ * hours are the review's Details rows already (AGL-3995), so the note does
407
+ * not say them twice: "Also from the client's details: Harbor Plumbing and
408
+ * the owner's own words."
409
+ */ export function aiSiteStartClientNote(intake) {
410
+ const parts = [
411
+ intake.businessName.trim(),
412
+ intake.ownerWords.trim() ? "the owner's own words" : ''
413
+ ].filter(Boolean);
414
+ if (!parts.length) return 'Built from the client’s details.';
415
+ return `Also from the client’s details: ${parts.join(' and ')}.`;
416
+ }
223
417
 
224
418
  //# sourceMappingURL=ai-site-start.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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 { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/** What a person answered; the shape the card holds and the brief is built from. */\nexport interface AiSiteStartAnswers {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` until they pick one, when the kind their first answer suggests is\n * the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n return null\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n kind: aiSiteKind(text('siteKind'))?.id ?? null,\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n }\n}\n"],"names":["aiSiteKind","aiSiteKindFor","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","kind","pages","submissions","welcomeEmail","aiSiteStartKind","answers","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","siteKind","businessName","city","brand","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","key","value","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","some"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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 { ClientBuildIntake } from '@aglyn/aglyn/app-utils/client-build'\nimport { weeklyHoursFromText, weeklyHoursToText } from '@aglyn/shared-ui-jsx/utils/weekly-hours'\nimport {\n AI_SITE_DETAIL_KEYS,\n AI_SITE_NO_DETAILS,\n aiSiteDetailProblem,\n aiSiteDetailsOf,\n type AiSiteDetailKey,\n type AiSiteDetails,\n} from './ai-site-details'\nimport { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INSPIRATION_INPUT,\n aiSiteInspirationInput,\n aiSiteInspirationLimit,\n aiSiteInspirationRefusal,\n aiSiteInspirationUrl,\n} from './ai-site-inspiration'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/**\n * What a person answered; the shape the card holds and the brief is built from.\n *\n * Beside the answers each step asks, the business details its \"More details\"\n * offers (AGL-3979, `AiSiteDetails`): every one optional and `''` until given,\n * the phone number and email on the Details step itself.\n */\nexport interface AiSiteStartAnswers extends AiSiteDetails {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` until they pick one, when the kind their first answer suggests is\n * the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /** The city or town the business is in (AGL-3979); empty when they did not say. */\n city: string\n /** The brand color, as a six-digit hex (AGL-3979); empty for the style's own. */\n brand: string\n /**\n * The sites they like (AGL-3975), as typed, one a field; absent or empty\n * for none. Asked only while `release_ai_site_inspiration` is on.\n */\n inspiration?: string[]\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n city: '',\n brand: '',\n ...AI_SITE_NO_DETAILS,\n}\n\n/** The answers a person may give beyond the ones each step asks, by key (AGL-3979). */\nexport type AiSiteStartExtraKey = AiSiteDetailKey | 'city' | 'brand'\n\n/** Every extra, in the order the review lists them. */\nexport const AI_SITE_START_EXTRA_KEYS: readonly AiSiteStartExtraKey[] = [\n 'phone',\n 'email',\n 'tagline',\n 'founded',\n 'serviceArea',\n 'priceRange',\n 'languages',\n 'brand',\n 'street',\n 'city',\n 'region',\n 'postalCode',\n 'country',\n 'hours',\n 'social',\n]\n\n/** What is wrong with one extra, worded for beside its field; `null` when it may be used. */\nexport function aiSiteStartExtraProblem(\n key: AiSiteStartExtraKey,\n value: string,\n): { code: 'too_long' | 'invalid'; message: string } | null {\n if (key === 'city' || key === 'brand') {\n return value.trim().length > AI_SITE_INPUT_MAX_CHARS\n ? { code: 'too_long', message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.` }\n : null\n }\n return aiSiteDetailProblem(key, value)\n}\n\n/** The extras a person gave, in the review's order: what \"More details\" was used for. */\nexport function aiSiteStartExtrasGiven(answers: AiSiteStartAnswers): AiSiteStartExtraKey[] {\n return AI_SITE_START_EXTRA_KEYS.filter((key) => (answers[key] ?? '').trim() !== '')\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n for (const key of AI_SITE_START_EXTRA_KEYS) {\n const problem = aiSiteStartExtraProblem(key, answers[key] ?? '')\n if (problem) return problem.message\n }\n return aiSiteStartInspirationRefusal(answers, options)\n}\n\n/** The sites they typed, without the empty fields. */\nexport function aiSiteStartInspirationTyped(answers: Pick<AiSiteStartAnswers, 'inspiration'>): string[] {\n return (answers.inspiration ?? []).map((entry) => entry.trim()).filter(Boolean)\n}\n\n/** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */\nexport function aiSiteStartInspirationRefusal(\n answers: Pick<AiSiteStartAnswers, 'inspiration'>,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const typed = aiSiteStartInspirationTyped(answers)\n if (typed.some((entry) => !aiSiteInspirationUrl(entry))) {\n return 'Give each site you like as its https address, like https://example.com.'\n }\n return aiSiteInspirationRefusal(typed, options.freeTaste)\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId' | 'inspiration' | AiSiteDetailKey> & Partial<AiSiteDetails> & { inspiration?: string } {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n // The details are sent only where given (AGL-3979): the plan step lists\n // every scalar input on a line of its own, and a start without them is\n // asked exactly what it was asked before.\n const details: Partial<AiSiteDetails> = {}\n for (const key of AI_SITE_DETAIL_KEYS) {\n const value = (answers[key] ?? '').trim()\n if (value) details[key] = value\n }\n // The sites they like travel only when there are some (AGL-3975), so a\n // start without them asks for exactly what it always did.\n const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers))\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: (answers.city ?? '').trim(),\n brand: (answers.brand ?? '').trim(),\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n ...details,\n ...(inspiration ? { [AI_SITE_INSPIRATION_INPUT]: inspiration } : {}),\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n ...AI_SITE_DETAIL_KEYS,\n AI_SITE_INSPIRATION_INPUT,\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n kind: aiSiteKind(text('siteKind'))?.id ?? null,\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n city: text('city'),\n brand: text('brand'),\n ...(Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key) => [key, text(key)])) as unknown as AiSiteDetails),\n // The sites they liked come back as typed, within the workspace's limit (AGL-3975).\n ...(text(AI_SITE_INSPIRATION_INPUT)\n ? { inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste)) }\n : {}),\n }\n}\n\n// ── A build staff run for a client (AGL-3958) ───────────────────────────────\n\n/** Shortens an answer to what a scalar input admits, on a word where it can. */\nfunction bounded(value: string, max: number): string {\n const trimmed = value.trim().replace(/\\s+/g, ' ')\n if (trimmed.length <= max) return trimmed\n const cut = trimmed.slice(0, max)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : cut).trim()\n}\n\n/** A time with am or pm: \"9am\", \"5:30 pm\", \"12 a.m.\". */\nconst TWELVE_HOUR_TIME = /\\b(\\d{1,2})(?::(\\d{2}))?\\s*([ap])\\.?m\\.?(?![a-z])/gi\n\n/**\n * The hours an owner gave, in the shape the Details step's hours picker\n * writes (`Tu-Sa 09:00-17:00`, one set of days a line), so the plan reads\n * them as it reads hours picked there. A time with am or pm is the time it\n * names, \"to\" joins a range as a dash does, and days with their hours may be\n * set apart by a comma, a semicolon or a line. Whatever still does not read\n * as hours — \"8–5\", \"Sat by appointment\" — is left out rather than guessed at.\n */\nfunction clientBuildHours(hours: string): string {\n const lines = hours\n .replace(TWELVE_HOUR_TIME, (time, hour: string, minutes: string | undefined, half: string) => {\n const h = Number(hour)\n if (h < 1 || h > 12) return time\n const clock = (h % 12) + (half.toLowerCase() === 'p' ? 12 : 0)\n return `${String(clock).padStart(2, '0')}:${minutes ?? '00'}`\n })\n .replace(/\\s+to\\s+/gi, '-')\n .replace(/(\\d{1,2}:\\d{2})\\s*,\\s*/g, '$1\\n')\n .replace(/;/g, '\\n')\n const week = weeklyHoursFromText(lines).hours\n return week.some((day) => day.length) ? weeklyHoursToText(week) : ''\n}\n\n/**\n * The towns an intake serves, one a line, as the Details step's \"Areas you\n * serve\" holds them: \"Austin, Round Rock and Pflugerville\" is three.\n */\nfunction clientBuildServiceArea(towns: string): string {\n return towns\n .split(/\\r?\\n|[,;]|\\s+(?:and|&)\\s+/i)\n .map((town) => town.trim())\n .filter(Boolean)\n .join('\\n')\n}\n\n/**\n * The questions' answers for a site staff are building for a client: what\n * the business does is the kind of site, and the details the intake holds\n * fill the Details step's own fields (AGL-3979, AGL-3995): the phone, the\n * email, the towns as the areas it serves and the hours, each held to that\n * detail's own rule, so one the Details step would refuse is left empty\n * there. Every other answer keeps its default, which the review shows to\n * change. The intake, a refused detail included, also reaches the job\n * through {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.\n */\nexport function aiSiteStartAnswersFromClientBuild(intake: ClientBuildIntake): AiSiteStartAnswers {\n const details = aiSiteDetailsOf({\n phone: intake.phone,\n email: intake.email,\n serviceArea: clientBuildServiceArea(intake.towns),\n hours: clientBuildHours(intake.hours),\n })\n return {\n ...AI_SITE_START_ANSWERS,\n siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS),\n phone: details.phone,\n email: details.email,\n serviceArea: details.serviceArea,\n hours: details.hours,\n }\n}\n\n/**\n * The per-site inputs the intake fills, which the guided start's own\n * questions leave empty: the business's name for the header and footer, and\n * the towns it serves as its place.\n *\n * The towns are the place only while no address is given. The Entity reads\n * `city` as the address's own locality wherever there is a street or a\n * postal code (`aiSiteEntity`), so once the answers carry either, `city` is\n * the city the Details step was given with that address, never the towns\n * (AGL-3995). The intake asks no address, so today that is the Details step's\n * own, typed on the review; the towns still reach the site as the areas it\n * serves.\n */\nexport function aiSiteStartClientInputs(\n intake: ClientBuildIntake,\n answers?: Pick<AiSiteStartAnswers, 'street' | 'postalCode' | 'city'> | null,\n): Pick<AiSiteJobInputs, 'businessName' | 'city'> {\n const addressed = Boolean(answers?.street.trim() || answers?.postalCode.trim())\n return {\n businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),\n city: bounded(addressed ? (answers?.city ?? '') : intake.towns, AI_SITE_INPUT_MAX_CHARS),\n }\n}\n\n/**\n * The intake as sentences added to the brief: how to reach the business and\n * when it is open, which the site should say where a visitor looks for them,\n * and the owner's own words, quoted, which the copy should sound like. Empty\n * answers say nothing.\n */\nexport function aiSiteStartClientBrief(intake: ClientBuildIntake): string {\n const name = intake.businessName.trim()\n const towns = intake.towns.trim()\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n const hours = intake.hours.trim()\n const words = intake.ownerWords.trim().replace(/\\s+/g, ' ')\n const sentences: string[] = []\n if (name) sentences.push(`The business is called ${name}.`)\n if (towns) sentences.push(`It serves ${towns}.`)\n const contact = [phone ? `call ${phone}` : '', email ? `email ${email}` : ''].filter(Boolean)\n if (contact.length) {\n sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`)\n }\n if (hours) sentences.push(`Its hours are ${hours}.`)\n if (words) sentences.push(`In the owner's own words: \"${words}\" Write the copy so it sounds like them.`)\n return sentences.join(' ')\n}\n\n/**\n * What the review says the client build adds beyond its own rows, so staff\n * see the intake is used without it being asked again: the business's name\n * and the owner's own words, which no row shows. The phone, email, towns and\n * hours are the review's Details rows already (AGL-3995), so the note does\n * not say them twice: \"Also from the client's details: Harbor Plumbing and\n * the owner's own words.\"\n */\nexport function aiSiteStartClientNote(intake: ClientBuildIntake): string {\n const parts = [intake.businessName.trim(), intake.ownerWords.trim() ? \"the owner's own words\" : ''].filter(Boolean)\n if (!parts.length) return 'Built from the client’s details.'\n return `Also from the client’s details: ${parts.join(' and ')}.`\n}\n"],"names":["weeklyHoursFromText","weeklyHoursToText","AI_SITE_DETAIL_KEYS","AI_SITE_NO_DETAILS","aiSiteDetailProblem","aiSiteDetailsOf","aiSiteKind","aiSiteKindFor","AI_SITE_INSPIRATION_INPUT","aiSiteInspirationInput","aiSiteInspirationLimit","aiSiteInspirationRefusal","aiSiteInspirationUrl","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","kind","pages","submissions","welcomeEmail","city","brand","AI_SITE_START_EXTRA_KEYS","aiSiteStartExtraProblem","key","value","trim","length","code","message","aiSiteStartExtrasGiven","answers","filter","aiSiteStartKind","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","Number","isInteger","min","max","includes","problem","aiSiteStartInspirationRefusal","aiSiteStartInspirationTyped","inspiration","map","Boolean","typed","some","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","details","businessType","starter","siteKind","businessName","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","fromEntries","split","slice","bounded","trimmed","replace","cut","space","lastIndexOf","TWELVE_HOUR_TIME","clientBuildHours","hours","lines","time","hour","minutes","half","h","clock","String","padStart","week","day","clientBuildServiceArea","towns","town","aiSiteStartAnswersFromClientBuild","intake","phone","email","serviceArea","whatTheyDo","aiSiteStartClientInputs","addressed","street","postalCode","aiSiteStartClientBrief","name","words","ownerWords","contact","aiSiteStartClientNote","parts"],"mappings":"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"}
@@ -78,8 +78,11 @@
78
78
  * The sentence that says where the feature comes from, and who can add it.
79
79
  * `feature` is what the entry is called: "Create with AI" unless it is an ask.
80
80
  */ export function aiUpsellOffer(canManageBilling, feature = AI_UPSELL_CREATE_LABEL) {
81
+ // Since AGL-3965 every paid plan's own credits pay for generation, so this
82
+ // door is reached only where it is switched off for the workspace and an
83
+ // Aglyn AI tier (which switches it on with its credits) is for sale.
81
84
  const addon = aiAddonName();
82
- return canManageBilling ? `${feature} comes with the ${addon} add-on, which you can add to this ` + 'workspace from Billing.' : `${feature} comes with the ${addon} add-on. Ask a workspace owner or admin ` + 'to add it from Billing.';
85
+ return canManageBilling ? `${feature} is switched off for this workspace. It comes with an ${addon} ` + 'tier, which you can choose in Billing.' : `${feature} is switched off for this workspace. It comes with an ${addon} ` + 'tier. Ask a workspace owner or admin to choose one in Billing.';
83
86
  }
84
87
 
85
88
  //# sourceMappingURL=ai-upsell-copy.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-upsell-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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AiBriefKind } from './ai-brief-copy'\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} comes with the ${addon} add-on, which you can add to this ` +\n 'workspace from Billing.'\n : `${feature} comes with the ${addon} add-on. Ask a workspace owner or admin ` +\n 'to add it from Billing.'\n}\n"],"names":["aiAddonName","AI_UPSELL_CREATE_LABEL","AI_UPSELL_COPY","page","title","does","template","layout","form","component","workflow","email","campaign","product","logic","overlay","funnel","insight","label","aiUpsellLabel","kind","aiUpsellOffer","canManageBilling","feature","addon"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,2CAA0C;AAyBtE,kDAAkD,GAClD,OAAO,MAAMC,yBAAyB,iBAAgB;AAEtD,kDAAkD,GAClD,OAAO,MAAMC,iBAA+D;IAC1EC,MAAM;QACJC,OAAO;QACPC,MACE,sFACA,kFACA;IACJ;IACAC,UAAU;QACRF,OAAO;QACPC,MACE,iFACA,wFACA;IACJ;IACAE,QAAQ;QACNH,OAAO;QACPC,MACE,0FACA,wFACA;IACJ;IACAG,MAAM;QACJJ,OAAO;QACPC,MACE,sFACA;IACJ;IACAI,WAAW;QACTL,OAAO;QACPC,MACE,wFACA,uFACA;IACJ;IACAK,UAAU;QACRN,OAAO;QACPC,MACE,kFACA;IACJ;IACAM,OAAO;QACLP,OAAO;QACPC,MACE,yFACA,yFACA;IACJ;IACAO,UAAU;QACRR,OAAO;QACPC,MACE,2FACA,2FACA;IACJ;IACAQ,SAAS;QACPT,OAAO;QACPC,MACE,0FACA,+FACA;IACJ;IACAS,OAAO;QACLV,OAAO;QACPC,MACE,yFACA;IACJ;IACAU,SAAS;QACPX,OAAO;QACPC,MACE,uFACA;IACJ;IACAW,QAAQ;QACNZ,OAAO;QACPC,MACE,wFACA;IACJ;IACAY,SAAS;QACPb,OAAO;QACPc,OAAO;QACPb,MACE,0FACA;IACJ;AACF,EAAC;AAED,uCAAuC,GACvC,OAAO,SAASc,cAAcC,IAAkB;QACvClB;IAAP,QAAOA,6BAAAA,cAAc,CAACkB,KAAK,CAACF,KAAK,YAA1BhB,6BAA8BD;AACvC;AAEA;;;CAGC,GACD,OAAO,SAASoB,cAAcC,gBAAyB,EAAEC,UAAUtB,sBAAsB;IACvF,MAAMuB,QAAQxB;IACd,OAAOsB,mBACH,GAAGC,QAAQ,gBAAgB,EAAEC,MAAM,mCAAmC,CAAC,GACrE,4BACF,GAAGD,QAAQ,gBAAgB,EAAEC,MAAM,wCAAwC,CAAC,GAC1E;AACR"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-upsell-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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AiBriefKind } from './ai-brief-copy'\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n // Since AGL-3965 every paid plan's own credits pay for generation, so this\n // door is reached only where it is switched off for the workspace and an\n // Aglyn AI tier (which switches it on with its credits) is for sale.\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} is switched off for this workspace. It comes with an ${addon} ` +\n 'tier, which you can choose in Billing.'\n : `${feature} is switched off for this workspace. It comes with an ${addon} ` +\n 'tier. Ask a workspace owner or admin to choose one in Billing.'\n}\n"],"names":["aiAddonName","AI_UPSELL_CREATE_LABEL","AI_UPSELL_COPY","page","title","does","template","layout","form","component","workflow","email","campaign","product","logic","overlay","funnel","insight","label","aiUpsellLabel","kind","aiUpsellOffer","canManageBilling","feature","addon"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,2CAA0C;AAyBtE,kDAAkD,GAClD,OAAO,MAAMC,yBAAyB,iBAAgB;AAEtD,kDAAkD,GAClD,OAAO,MAAMC,iBAA+D;IAC1EC,MAAM;QACJC,OAAO;QACPC,MACE,sFACA,kFACA;IACJ;IACAC,UAAU;QACRF,OAAO;QACPC,MACE,iFACA,wFACA;IACJ;IACAE,QAAQ;QACNH,OAAO;QACPC,MACE,0FACA,wFACA;IACJ;IACAG,MAAM;QACJJ,OAAO;QACPC,MACE,sFACA;IACJ;IACAI,WAAW;QACTL,OAAO;QACPC,MACE,wFACA,uFACA;IACJ;IACAK,UAAU;QACRN,OAAO;QACPC,MACE,kFACA;IACJ;IACAM,OAAO;QACLP,OAAO;QACPC,MACE,yFACA,yFACA;IACJ;IACAO,UAAU;QACRR,OAAO;QACPC,MACE,2FACA,2FACA;IACJ;IACAQ,SAAS;QACPT,OAAO;QACPC,MACE,0FACA,+FACA;IACJ;IACAS,OAAO;QACLV,OAAO;QACPC,MACE,yFACA;IACJ;IACAU,SAAS;QACPX,OAAO;QACPC,MACE,uFACA;IACJ;IACAW,QAAQ;QACNZ,OAAO;QACPC,MACE,wFACA;IACJ;IACAY,SAAS;QACPb,OAAO;QACPc,OAAO;QACPb,MACE,0FACA;IACJ;AACF,EAAC;AAED,uCAAuC,GACvC,OAAO,SAASc,cAAcC,IAAkB;QACvClB;IAAP,QAAOA,6BAAAA,cAAc,CAACkB,KAAK,CAACF,KAAK,YAA1BhB,6BAA8BD;AACvC;AAEA;;;CAGC,GACD,OAAO,SAASoB,cAAcC,gBAAyB,EAAEC,UAAUtB,sBAAsB;IACvF,2EAA2E;IAC3E,yEAAyE;IACzE,qEAAqE;IACrE,MAAMuB,QAAQxB;IACd,OAAOsB,mBACH,GAAGC,QAAQ,sDAAsD,EAAEC,MAAM,CAAC,CAAC,GACzE,2CACF,GAAGD,QAAQ,sDAAsD,EAAEC,MAAM,CAAC,CAAC,GACzE;AACR"}
@@ -61,9 +61,10 @@ declare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {
61
61
  * `tier-margin-floor.spec.ts` is the model, and it pins both the rule and
62
62
  * the resulting figures.
63
63
  *
64
- * The Aglyn AI add-on (AGL-2896) adds `AI_ADDON_CREDITS_PER_MONTH[plan]`
65
- * to this band in `resolveOrgEntitlements` — one pool, widened, rather
66
- * than a second meter.
64
+ * ONE POOL FOR EVERYTHING (AGL-3965): this band pays for every Aglyn AI
65
+ * action, generation included, on every plan. An Aglyn AI tier adds its
66
+ * credits (`AI_TIERS[tier].credits`) to it in `resolveOrgEntitlements`
67
+ * — one pool, widened, rather than a second meter.
67
68
  */
68
69
  assistCreditsPerMonth?: number;
69
70
  }
@@ -124,18 +124,13 @@
124
124
  // plan bands at zero any more" was true of the number and false of the
125
125
  // capability.
126
126
  //
127
- // It opens `aiAssist` rather than `aiGenerative` because that is the shape
128
- // of every paid rung above it — the included band funds the guided
129
- // assistant and generation is the add-on. Opening the generative door here
130
- // instead would hand the FIRST paid rung a door Pro through Agency have to
131
- // buy.
127
+ // Since AGL-3965 it opens `aiGenerative` too, as every paid rung does: the
128
+ // included band pays for generation and assist alike (one pool), and an
129
+ // Aglyn AI tier adds credits on top rather than opening a door.
132
130
  //
133
131
  // No cost moves: `tier-margin-floor.spec.ts` already prices this tier's
134
132
  // assist term at 100% burn of the 750 credits, 75¢/mo. A door does not
135
133
  // widen a band; it only makes spend that was already modeled reachable.
136
- //
137
- // The AI add-on adds `AI_ADDON_CREDITS_PER_MONTH.starter` on top and
138
- // switches `aiGenerative` on as well.
139
134
  starter: 750,
140
135
  // $2.75 of provider spend, against the $8.35 the tier's other seven cost
141
136
  // terms leave out of $56. See the key's docblock for why the remainder and
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, before the catalog file core reads them from exists in\n// its new shape, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE AI PLUGIN'S PLAN BAND (AGL-3080): how many AI credits each plan\n * includes a month. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`), so `/pricing`, the plan comparison\n * and every gate read the same figure they always have; this is where it is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * AI credits per calendar month — the band assist spends against.\n *\n * A CREDIT IS A UNIT OF MODEL COST, not a message. One assist action can\n * cost two orders of magnitude more than another: a question is a few\n * thousand tokens, while generating a screen carries the node tree, the\n * component catalog and the theme tokens in, structured markup out, and\n * iterates. A message allowance would price those the same, so one\n * workspace's ten screen builds would outspend another's thousand\n * questions and both would read as \"within allowance\".\n *\n * The unit is defined by `ASSIST_CREDIT_COST_USD` and the conversion\n * lives in `assist-credits.ts`. Only that module turns credits into\n * dollars; everything a customer sees counts credits, because the dollar\n * figure behind them is our provider bill and not a price.\n *\n * ## HOW THE BAND IS SIZED, and why it is not a share of the price\n *\n * A credit is a dollar of provider spend at `ASSIST_CREDIT_COST_USD`, so\n * the band IS a liability figure: the plan's whole assist give is\n * `assistCreditsPerMonth / 1000` dollars. Credits past it are not a give\n * — since AGL-2653 they are SOLD at `extraAssistCreditsUsdPer1k`, unless\n * the org's `assistOverage.hardCap` asks `reserveAssistMessage` to refuse\n * at the band instead. Sizing it as a share of the subscription price\n * says nothing about whether the tier can afford it, because the price is\n * also carrying storage, bandwidth, form submissions, dataset storage,\n * API requests, contacts and email.\n *\n * So each band is sized against what those SEVEN other terms leave.\n * Every paid band takes between a quarter and a third of that remainder,\n * which keeps the tier's worst case positive and keeps the ladder's\n * shape: the margin each tier holds at 100% of every band stays in\n * proportion to what it held before assist was sold at all.\n * `tier-margin-floor.spec.ts` is the model, and it pins both the rule and\n * the resulting figures.\n *\n * The Aglyn AI add-on (AGL-2896) adds `AI_ADDON_CREDITS_PER_MONTH[plan]`\n * to this band in `resolveOrgEntitlements` — one pool, widened, rather\n * than a second meter.\n */\n assistCreditsPerMonth?: number\n }\n}\n\n/**\n * AI credits an Enterprise agreement includes per month, before the contract\n * says otherwise.\n *\n * ## Never `UNLIMITED`, and the reason is not stylistic\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is\n * `null`, and `Number(null)` is `0`. An unbounded assist band therefore\n * serialises to a band of ZERO through every route that does not also send\n * the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the\n * only customers with a signed contract the one budget that refuses\n * everything. A finite number crosses the wire as itself.\n *\n * The band is also a real liability rather than a capacity we already own.\n * Storage and page views are infrastructure metered into the deal; assist is\n * a per-token charge from a third party, and generative building spends it in\n * units two orders of magnitude apart.\n *\n * ## Why this number\n *\n * Enterprise carries no list price, so the band cannot be sized the way every\n * self-serve tier's is — against what that tier's other cost terms leave out\n * of a known price. It is anchored to the top of the ladder instead: 116,000\n * credits is twice Agency's band, the rule every Enterprise fallback follows\n * since the 2026-09-07 pricing decision (see the `enterprise` row of\n * `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or\n * under 9% of any deal priced at or above the self-serve top. A deal below\n * that is sold as Agency, not written as an Enterprise agreement.\n *\n * ## It is a DEFAULT, and a contract raises it\n *\n * `resolveOrgEntitlements` applies a per-org\n * `entitlements.assistCreditsPerMonth` override ahead of this, so a deal that\n * buys more assist buys it on that org without moving the figure every other\n * agreement is measured against — the same mechanism the other contracted\n * bands use.\n */\nexport const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116_000\n\n/**\n * The Free AI taste (AGL-2925): the credits a Free workspace draws on each\n * month, behind a hard wall.\n *\n * ## Why it exists\n *\n * So the AI landing pages can say \"generate your first page free\" and mean\n * it. Three hundred credits is one or two generated sections or a handful of\n * copy rewrites — enough to see what the assistant does, not enough to build\n * a site on. The band is a WALL: `PLAN_PRICING.free\n * .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers\n * true and the reservation refuses at 100% with no overage, no switch and no\n * invoice. Nothing about a Free workspace can produce a charge.\n *\n * ## What it costs, and why that is the number\n *\n * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs\n * at most **$0.30 per Free workspace per month** — asserted by\n * `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with\n * no invoice behind it, which is why the same issue closes every multiplier\n * a script could use before the flag flips:\n *\n * - it is metered per ACCOUNT as well as per workspace, so the three free\n * workspaces an account may hold (AGL-2265) share one 300-credit\n * allowance rather than tripling it (`reserveAssistMessage`);\n * - every free request is bounded per IP, per uid and per account per\n * day (a minimum account age, `AI_FREE_MIN_ACCOUNT_AGE_HOURS`, is\n * available and off by default — AGL-3591);\n * - a platform-wide daily ceiling on free spend pauses the taste for\n * everyone until the UTC day rolls, paid workspaces untouched.\n *\n * The account allowance reads THIS constant rather than the workspace's\n * resolved band, because a staff override that widens one workspace's band\n * is a decision about that workspace, not about how much one person may draw\n * across all of theirs.\n */\nexport const FREE_AI_TASTE_CREDITS_PER_MONTH = 300\n\n/** The AI credits each plan includes a month — see the key's own docblock. */\nexport const AI_CREDITS_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // The Free AI taste (AGL-2925): a real band, and a wall. See the constant\n // for what it buys, what it costs and the precautions that keep one person\n // from drawing it three times over.\n free: FREE_AI_TASTE_CREDITS_PER_MONTH,\n // The first paid rung of the assist ladder (AGL-3203). Starter banded at 0\n // until 2026-09-20, which put a PAYING workspace below the Free taste's 300\n // on the one axis a visitor compares straight down the column — the\n // `/pricing` row read `300 / mo` for Free and `—` for Starter. A paid plan\n // that includes less of something than the free plan is not a packaging\n // subtlety; it is the comparison table arguing against the upgrade.\n //\n // 750 is deliberately small: 2.5x the taste, enough that the assistant is a\n // thing the tier HAS rather than a thing it is shown, and $0.75 of provider\n // spend a month at `ASSIST_CREDIT_COST_USD` against a $16 annual price — a\n // twentieth of the $8.81 the tier's other metered bands already cost, and\n // the widest margin on the ladder absorbs it with room left\n // (`tier-margin-floor.spec.ts` carries the arithmetic).\n //\n // It is NOT a wall, unlike Free's: `PLAN_PRICING.starter\n // .extraAssistCreditsUsdPer1k` is $3.00, so past 750 the tier meters and\n // bills like every paid plan above it. The two fields move together by rule\n // — a positive band with no rate beside it is usage past a bound that is\n // silently free, which `plan-entitlements.spec.ts` forbids.\n //\n // `features.aiAssist` is TRUE, and AGL-3203 shipping it false was the\n // defect AGL-3207 closes. The reasoning then was \"the band is credits, not\n // the guided rung, exactly as on Free\" — but on Free the band IS spendable,\n // through `aiGenerative`, which is the door the taste exists for. Starter\n // carried neither flag, so it was the only row on the ladder sold a band\n // with nothing to spend it through: 750 credits reachable only by buying\n // the $9 add-on that was also the only thing that made them usable. \"No\n // plan bands at zero any more\" was true of the number and false of the\n // capability.\n //\n // It opens `aiAssist` rather than `aiGenerative` because that is the shape\n // of every paid rung above it — the included band funds the guided\n // assistant and generation is the add-on. Opening the generative door here\n // instead would hand the FIRST paid rung a door Pro through Agency have to\n // buy.\n //\n // No cost moves: `tier-margin-floor.spec.ts` already prices this tier's\n // assist term at 100% burn of the 750 credits, 75¢/mo. A door does not\n // widen a band; it only makes spend that was already modeled reachable.\n //\n // The AI add-on adds `AI_ADDON_CREDITS_PER_MONTH.starter` on top and\n // switches `aiGenerative` on as well.\n starter: 750,\n // $2.75 of provider spend, against the $8.35 the tier's other seven cost\n // terms leave out of $56. See the key's docblock for why the remainder and\n // not the price is what sizes this.\n pro: 2_750,\n // $7.50, against the $22.69 the other seven terms leave out of $139.\n business: 7_500,\n // $10.00, against the $30.62 the other seven terms leave out of $249.\n scale: 10_000,\n // $13.00, against the $39.74 the other seven terms leave out of $399 — the\n // thinnest remainder on the ladder, and the tier that binds the whole\n // assist ladder's share of it.\n advanced: 13_000,\n // $58.00, against the $176.71 the other seven terms leave out of $1,299.\n agency: 58_000,\n enterprise: ENTERPRISE_ASSIST_CREDITS_PER_MONTH,\n}\n\n/** The AI plugin's plan figures, for the manifest generator. */\nexport function aiPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'assistCreditsPerMonth',\n label: 'AI credits / mo',\n byPlan: AI_CREDITS_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["ENTERPRISE_ASSIST_CREDITS_PER_MONTH","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDITS_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","aiPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AAyDrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,MAAMA,sCAAsC,OAAO;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AAElD,4EAA4E,GAC5E,OAAO,MAAMC,qBAAwD;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,oCAAoC;IACpCC,MAAMF;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,oEAAoE;IACpE,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,wDAAwD;IACxD,EAAE;IACF,yDAAyD;IACzD,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,4DAA4D;IAC5D,EAAE;IACF,sEAAsE;IACtE,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,uEAAuE;IACvE,cAAc;IACd,EAAE;IACF,2EAA2E;IAC3E,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,OAAO;IACP,EAAE;IACF,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,EAAE;IACF,qEAAqE;IACrE,sCAAsC;IACtCG,SAAS;IACT,yEAAyE;IACzE,2EAA2E;IAC3E,oCAAoC;IACpCC,KAAK;IACL,qEAAqE;IACrEC,UAAU;IACV,sEAAsE;IACtEC,OAAO;IACP,2EAA2E;IAC3E,sEAAsE;IACtE,+BAA+B;IAC/BC,UAAU;IACV,yEAAyE;IACzEC,QAAQ;IACRC,YAAYV;AACd,EAAC;AAED,8DAA8D,GAC9D,OAAO,SAASW;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, before the catalog file core reads them from exists in\n// its new shape, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE AI PLUGIN'S PLAN BAND (AGL-3080): how many AI credits each plan\n * includes a month. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`), so `/pricing`, the plan comparison\n * and every gate read the same figure they always have; this is where it is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * AI credits per calendar month — the band assist spends against.\n *\n * A CREDIT IS A UNIT OF MODEL COST, not a message. One assist action can\n * cost two orders of magnitude more than another: a question is a few\n * thousand tokens, while generating a screen carries the node tree, the\n * component catalog and the theme tokens in, structured markup out, and\n * iterates. A message allowance would price those the same, so one\n * workspace's ten screen builds would outspend another's thousand\n * questions and both would read as \"within allowance\".\n *\n * The unit is defined by `ASSIST_CREDIT_COST_USD` and the conversion\n * lives in `assist-credits.ts`. Only that module turns credits into\n * dollars; everything a customer sees counts credits, because the dollar\n * figure behind them is our provider bill and not a price.\n *\n * ## HOW THE BAND IS SIZED, and why it is not a share of the price\n *\n * A credit is a dollar of provider spend at `ASSIST_CREDIT_COST_USD`, so\n * the band IS a liability figure: the plan's whole assist give is\n * `assistCreditsPerMonth / 1000` dollars. Credits past it are not a give\n * — since AGL-2653 they are SOLD at `extraAssistCreditsUsdPer1k`, unless\n * the org's `assistOverage.hardCap` asks `reserveAssistMessage` to refuse\n * at the band instead. Sizing it as a share of the subscription price\n * says nothing about whether the tier can afford it, because the price is\n * also carrying storage, bandwidth, form submissions, dataset storage,\n * API requests, contacts and email.\n *\n * So each band is sized against what those SEVEN other terms leave.\n * Every paid band takes between a quarter and a third of that remainder,\n * which keeps the tier's worst case positive and keeps the ladder's\n * shape: the margin each tier holds at 100% of every band stays in\n * proportion to what it held before assist was sold at all.\n * `tier-margin-floor.spec.ts` is the model, and it pins both the rule and\n * the resulting figures.\n *\n * ONE POOL FOR EVERYTHING (AGL-3965): this band pays for every Aglyn AI\n * action, generation included, on every plan. An Aglyn AI tier adds its\n * credits (`AI_TIERS[tier].credits`) to it in `resolveOrgEntitlements`\n * — one pool, widened, rather than a second meter.\n */\n assistCreditsPerMonth?: number\n }\n}\n\n/**\n * AI credits an Enterprise agreement includes per month, before the contract\n * says otherwise.\n *\n * ## Never `UNLIMITED`, and the reason is not stylistic\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is\n * `null`, and `Number(null)` is `0`. An unbounded assist band therefore\n * serialises to a band of ZERO through every route that does not also send\n * the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the\n * only customers with a signed contract the one budget that refuses\n * everything. A finite number crosses the wire as itself.\n *\n * The band is also a real liability rather than a capacity we already own.\n * Storage and page views are infrastructure metered into the deal; assist is\n * a per-token charge from a third party, and generative building spends it in\n * units two orders of magnitude apart.\n *\n * ## Why this number\n *\n * Enterprise carries no list price, so the band cannot be sized the way every\n * self-serve tier's is — against what that tier's other cost terms leave out\n * of a known price. It is anchored to the top of the ladder instead: 116,000\n * credits is twice Agency's band, the rule every Enterprise fallback follows\n * since the 2026-09-07 pricing decision (see the `enterprise` row of\n * `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or\n * under 9% of any deal priced at or above the self-serve top. A deal below\n * that is sold as Agency, not written as an Enterprise agreement.\n *\n * ## It is a DEFAULT, and a contract raises it\n *\n * `resolveOrgEntitlements` applies a per-org\n * `entitlements.assistCreditsPerMonth` override ahead of this, so a deal that\n * buys more assist buys it on that org without moving the figure every other\n * agreement is measured against — the same mechanism the other contracted\n * bands use.\n */\nexport const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116_000\n\n/**\n * The Free AI taste (AGL-2925): the credits a Free workspace draws on each\n * month, behind a hard wall.\n *\n * ## Why it exists\n *\n * So the AI landing pages can say \"generate your first page free\" and mean\n * it. Three hundred credits is one or two generated sections or a handful of\n * copy rewrites — enough to see what the assistant does, not enough to build\n * a site on. The band is a WALL: `PLAN_PRICING.free\n * .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers\n * true and the reservation refuses at 100% with no overage, no switch and no\n * invoice. Nothing about a Free workspace can produce a charge.\n *\n * ## What it costs, and why that is the number\n *\n * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs\n * at most **$0.30 per Free workspace per month** — asserted by\n * `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with\n * no invoice behind it, which is why the same issue closes every multiplier\n * a script could use before the flag flips:\n *\n * - it is metered per ACCOUNT as well as per workspace, so the three free\n * workspaces an account may hold (AGL-2265) share one 300-credit\n * allowance rather than tripling it (`reserveAssistMessage`);\n * - every free request is bounded per IP, per uid and per account per\n * day (a minimum account age, `AI_FREE_MIN_ACCOUNT_AGE_HOURS`, is\n * available and off by default — AGL-3591);\n * - a platform-wide daily ceiling on free spend pauses the taste for\n * everyone until the UTC day rolls, paid workspaces untouched.\n *\n * The account allowance reads THIS constant rather than the workspace's\n * resolved band, because a staff override that widens one workspace's band\n * is a decision about that workspace, not about how much one person may draw\n * across all of theirs.\n */\nexport const FREE_AI_TASTE_CREDITS_PER_MONTH = 300\n\n/** The AI credits each plan includes a month — see the key's own docblock. */\nexport const AI_CREDITS_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // The Free AI taste (AGL-2925): a real band, and a wall. See the constant\n // for what it buys, what it costs and the precautions that keep one person\n // from drawing it three times over.\n free: FREE_AI_TASTE_CREDITS_PER_MONTH,\n // The first paid rung of the assist ladder (AGL-3203). Starter banded at 0\n // until 2026-09-20, which put a PAYING workspace below the Free taste's 300\n // on the one axis a visitor compares straight down the column — the\n // `/pricing` row read `300 / mo` for Free and `—` for Starter. A paid plan\n // that includes less of something than the free plan is not a packaging\n // subtlety; it is the comparison table arguing against the upgrade.\n //\n // 750 is deliberately small: 2.5x the taste, enough that the assistant is a\n // thing the tier HAS rather than a thing it is shown, and $0.75 of provider\n // spend a month at `ASSIST_CREDIT_COST_USD` against a $16 annual price — a\n // twentieth of the $8.81 the tier's other metered bands already cost, and\n // the widest margin on the ladder absorbs it with room left\n // (`tier-margin-floor.spec.ts` carries the arithmetic).\n //\n // It is NOT a wall, unlike Free's: `PLAN_PRICING.starter\n // .extraAssistCreditsUsdPer1k` is $3.00, so past 750 the tier meters and\n // bills like every paid plan above it. The two fields move together by rule\n // — a positive band with no rate beside it is usage past a bound that is\n // silently free, which `plan-entitlements.spec.ts` forbids.\n //\n // `features.aiAssist` is TRUE, and AGL-3203 shipping it false was the\n // defect AGL-3207 closes. The reasoning then was \"the band is credits, not\n // the guided rung, exactly as on Free\" — but on Free the band IS spendable,\n // through `aiGenerative`, which is the door the taste exists for. Starter\n // carried neither flag, so it was the only row on the ladder sold a band\n // with nothing to spend it through: 750 credits reachable only by buying\n // the $9 add-on that was also the only thing that made them usable. \"No\n // plan bands at zero any more\" was true of the number and false of the\n // capability.\n //\n // Since AGL-3965 it opens `aiGenerative` too, as every paid rung does: the\n // included band pays for generation and assist alike (one pool), and an\n // Aglyn AI tier adds credits on top rather than opening a door.\n //\n // No cost moves: `tier-margin-floor.spec.ts` already prices this tier's\n // assist term at 100% burn of the 750 credits, 75¢/mo. A door does not\n // widen a band; it only makes spend that was already modeled reachable.\n starter: 750,\n // $2.75 of provider spend, against the $8.35 the tier's other seven cost\n // terms leave out of $56. See the key's docblock for why the remainder and\n // not the price is what sizes this.\n pro: 2_750,\n // $7.50, against the $22.69 the other seven terms leave out of $139.\n business: 7_500,\n // $10.00, against the $30.62 the other seven terms leave out of $249.\n scale: 10_000,\n // $13.00, against the $39.74 the other seven terms leave out of $399 — the\n // thinnest remainder on the ladder, and the tier that binds the whole\n // assist ladder's share of it.\n advanced: 13_000,\n // $58.00, against the $176.71 the other seven terms leave out of $1,299.\n agency: 58_000,\n enterprise: ENTERPRISE_ASSIST_CREDITS_PER_MONTH,\n}\n\n/** The AI plugin's plan figures, for the manifest generator. */\nexport function aiPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'assistCreditsPerMonth',\n label: 'AI credits / mo',\n byPlan: AI_CREDITS_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["ENTERPRISE_ASSIST_CREDITS_PER_MONTH","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDITS_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","aiPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AA0DrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,MAAMA,sCAAsC,OAAO;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AAElD,4EAA4E,GAC5E,OAAO,MAAMC,qBAAwD;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,oCAAoC;IACpCC,MAAMF;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,oEAAoE;IACpE,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,wDAAwD;IACxD,EAAE;IACF,yDAAyD;IACzD,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,4DAA4D;IAC5D,EAAE;IACF,sEAAsE;IACtE,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,uEAAuE;IACvE,cAAc;IACd,EAAE;IACF,2EAA2E;IAC3E,wEAAwE;IACxE,gEAAgE;IAChE,EAAE;IACF,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxEG,SAAS;IACT,yEAAyE;IACzE,2EAA2E;IAC3E,oCAAoC;IACpCC,KAAK;IACL,qEAAqE;IACrEC,UAAU;IACV,sEAAsE;IACtEC,OAAO;IACP,2EAA2E;IAC3E,sEAAsE;IACtE,+BAA+B;IAC/BC,UAAU;IACV,yEAAyE;IACzEC,QAAQ;IACRC,YAAYV;AACd,EAAC;AAED,8DAA8D,GAC9D,OAAO,SAASW;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
@@ -71,7 +71,14 @@ export interface GenerateAiDatasetInput extends AiDatasetPromptInput {
71
71
  }
72
72
  /** Design one dataset. It writes nothing to the site. */
73
73
  export declare function generateAiDataset(input: GenerateAiDatasetInput): Promise<AiValidatedGeneration<AiDataset>>;
74
- /** A designed dataset as the data plugin's `dataset` writer takes it: values by field name. */
74
+ /**
75
+ * A designed dataset as the data plugin's `dataset` writer takes it: values by
76
+ * field name. Its words' "a" and "an" agree with what follows them in English
77
+ * (`aiLayoutArticles`, AGL-3988), as a page's do: the beta.270 Sugar Hollow
78
+ * packages said "A easygoing session" in their records and on every page
79
+ * that listed them.
80
+ */
75
81
  export declare function aiDatasetDraftContent(name: string, dataset: AiDataset, options: {
76
82
  recordPages: boolean;
83
+ language?: string | null;
77
84
  }): Record<string, unknown>;