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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +2 -2
  2. package/package.json +18 -18
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +8 -1
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  8. package/src/lib/billing/ai-month-meter.js +89 -0
  9. package/src/lib/billing/ai-month-meter.js.map +1 -0
  10. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  11. package/src/lib/billing/ai-overage-close.js +4 -3
  12. package/src/lib/billing/ai-overage-close.js.map +1 -1
  13. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  14. package/src/lib/billing/ai-overage-cutover.js +1 -28
  15. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  16. package/src/lib/billing/ai-overage-ledger.js +1 -1
  17. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  18. package/src/lib/billing/ai-overage-standing.js +1 -1
  19. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  20. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  21. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  22. package/src/lib/components/ai-assist-provider.component.js +5 -4
  23. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  24. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  25. package/src/lib/components/ai-automation-zones.js +31 -0
  26. package/src/lib/components/ai-automation-zones.js.map +1 -0
  27. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  28. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  29. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  30. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  31. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  32. package/src/lib/components/ai-describe-button.component.js +4 -1
  33. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  34. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  35. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  36. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  37. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  38. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  39. package/src/lib/components/ai-job-plan.component.js +21 -4
  40. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  41. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  42. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  43. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  44. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  45. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  46. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  47. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  48. package/src/lib/components/ai-product-zones.d.ts +124 -0
  49. package/src/lib/components/ai-product-zones.js +32 -0
  50. package/src/lib/components/ai-product-zones.js.map +1 -0
  51. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  52. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  53. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  54. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  55. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  56. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  57. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  60. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  61. package/src/lib/components/assist-panel.component.js +2 -2
  62. package/src/lib/components/assist-panel.component.js.map +1 -1
  63. package/src/lib/components/assist-signals-page.component.js +2 -2
  64. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  65. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  66. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  67. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  68. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  69. package/src/lib/constants.d.ts +5 -0
  70. package/src/lib/constants.js +4 -0
  71. package/src/lib/constants.js.map +1 -1
  72. package/src/lib/declarations.js +2 -0
  73. package/src/lib/declarations.js.map +1 -1
  74. package/src/lib/declarations.server.js +24 -10
  75. package/src/lib/declarations.server.js.map +1 -1
  76. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  77. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  78. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  79. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  80. package/src/lib/insights/ai-figure-readers.js +2 -391
  81. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  82. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  83. package/src/lib/insights/ai-insight-digest.js +16 -4
  84. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  85. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  86. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  87. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  88. package/src/lib/jobs/ai-email-bindings.js +75 -42
  89. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  90. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  91. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  92. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  93. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  94. package/src/lib/jobs/ai-job-component-step.js +32 -9
  95. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  96. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  97. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  98. package/src/lib/jobs/ai-job-drafts.js +1 -1
  99. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  100. package/src/lib/jobs/ai-job-email-step.d.ts +5 -3
  101. package/src/lib/jobs/ai-job-email-step.js +11 -4
  102. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  104. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  105. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  107. package/src/lib/jobs/ai-job-form-step.js +60 -8
  108. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  109. package/src/lib/jobs/ai-job-generation.d.ts +22 -1
  110. package/src/lib/jobs/ai-job-generation.js +63 -3
  111. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  112. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  113. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  114. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  115. package/src/lib/jobs/ai-job-layout-step.js +46 -13
  116. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  117. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  118. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  119. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  120. package/src/lib/jobs/ai-job-page-step.js +7 -2
  121. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  122. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  123. package/src/lib/jobs/ai-job-plan-conformance.js +40 -1
  124. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  125. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  126. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  127. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  128. package/src/lib/jobs/ai-job-plan-step.d.ts +11 -2
  129. package/src/lib/jobs/ai-job-plan-step.js +49 -23
  130. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  131. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  132. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  133. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  134. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  135. package/src/lib/jobs/ai-job-products-step.js +54 -32
  136. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  137. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  138. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  139. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  140. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  141. package/src/lib/jobs/ai-job-site-step.js +6 -6
  142. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  143. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  144. package/src/lib/jobs/ai-job-template-step.js +73 -15
  145. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  146. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  147. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  148. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  149. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  150. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  151. package/src/lib/jobs/ai-jobs.js +1 -1
  152. package/src/lib/jobs/ai-jobs.js.map +1 -1
  153. package/src/lib/jobs/ai-workflow-records.d.ts +20 -11
  154. package/src/lib/jobs/ai-workflow-records.js +80 -43
  155. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  156. package/src/lib/model/ai-allotments.d.ts +27 -6
  157. package/src/lib/model/ai-allotments.js +54 -14
  158. package/src/lib/model/ai-allotments.js.map +1 -1
  159. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  160. package/src/lib/model/ai-automation-outline.js.map +1 -1
  161. package/src/lib/model/ai-build-plan.d.ts +57 -0
  162. package/src/lib/model/ai-build-plan.js +116 -7
  163. package/src/lib/model/ai-build-plan.js.map +1 -1
  164. package/src/lib/model/ai-job-creations.d.ts +55 -0
  165. package/src/lib/model/ai-job-creations.js +85 -0
  166. package/src/lib/model/ai-job-creations.js.map +1 -0
  167. package/src/lib/model/ai-page-job.d.ts +3 -2
  168. package/src/lib/model/ai-page-job.js +4 -3
  169. package/src/lib/model/ai-page-job.js.map +1 -1
  170. package/src/lib/model/ai-seo.d.ts +11 -51
  171. package/src/lib/model/ai-seo.js +0 -17
  172. package/src/lib/model/ai-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-inventory.js +1 -1
  174. package/src/lib/model/ai-site-inventory.js.map +1 -1
  175. package/src/lib/model/ai-site-job.d.ts +4 -2
  176. package/src/lib/model/ai-site-job.js +8 -4
  177. package/src/lib/model/ai-site-job.js.map +1 -1
  178. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  179. package/src/lib/model/ai-template-subjects.js +16 -2
  180. package/src/lib/model/ai-template-subjects.js.map +1 -1
  181. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  182. package/src/lib/model/ai-theme-proposal.js +1 -1
  183. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  184. package/src/lib/model/ai-tokens.js +1 -1
  185. package/src/lib/model/ai-tokens.js.map +1 -1
  186. package/src/lib/model/ai-usage-by-user.js +1 -1
  187. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  188. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  189. package/src/lib/model/ai-workflow-job.js +1 -2
  190. package/src/lib/model/ai-workflow-job.js.map +1 -1
  191. package/src/lib/plan-entitlements.d.ts +149 -0
  192. package/src/lib/plan-entitlements.js +168 -0
  193. package/src/lib/plan-entitlements.js.map +1 -0
  194. package/src/lib/plugin.js +1 -1
  195. package/src/lib/plugin.js.map +1 -1
  196. package/src/lib/providers/catalog.js +1 -1
  197. package/src/lib/providers/catalog.js.map +1 -1
  198. package/src/lib/providers/model-choice.js +1 -1
  199. package/src/lib/providers/model-choice.js.map +1 -1
  200. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  201. package/src/lib/runtime/ai-device-audit.js +1 -1
  202. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  203. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  204. package/src/lib/runtime/ai-doctrine-validators.js +178 -21
  205. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  206. package/src/lib/runtime/ai-doctrine.js +5 -5
  207. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  208. package/src/lib/runtime/ai-eval-live.js +30 -15
  209. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  210. package/src/lib/runtime/ai-eval.d.ts +9 -2
  211. package/src/lib/runtime/ai-eval.js +29 -2
  212. package/src/lib/runtime/ai-eval.js.map +1 -1
  213. package/src/lib/runtime/ai-gate.js +1 -1
  214. package/src/lib/runtime/ai-gate.js.map +1 -1
  215. package/src/lib/runtime/ai-node-tree.js +3 -3
  216. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  217. package/src/lib/runtime/ai-palette.d.ts +7 -1
  218. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  219. package/src/lib/runtime/ai-palette.generated.js +16 -13
  220. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  221. package/src/lib/runtime/ai-palette.js +7 -1
  222. package/src/lib/runtime/ai-palette.js.map +1 -1
  223. package/src/lib/runtime/seo-audit.d.ts +13 -63
  224. package/src/lib/runtime/seo-audit.js +22 -207
  225. package/src/lib/runtime/seo-audit.js.map +1 -1
  226. package/src/lib/runtime/site-inventory.js +29 -23
  227. package/src/lib/runtime/site-inventory.js.map +1 -1
  228. package/src/lib/server/ai-admin-org.js +1 -1
  229. package/src/lib/server/ai-admin-org.js.map +1 -1
  230. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  231. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  232. package/src/lib/server/ai-admin-signals.js +1 -1
  233. package/src/lib/server/ai-admin-signals.js.map +1 -1
  234. package/src/lib/server/ai-allotments.js +1 -1
  235. package/src/lib/server/ai-allotments.js.map +1 -1
  236. package/src/lib/server/ai-assist-prompts.js +3 -3
  237. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  238. package/src/lib/server/ai-assist.js +2 -2
  239. package/src/lib/server/ai-assist.js.map +1 -1
  240. package/src/lib/server/assist-chat.d.ts +1 -1
  241. package/src/lib/server/assist-chat.js +13 -12
  242. package/src/lib/server/assist-chat.js.map +1 -1
  243. package/src/lib/server/assist-view-context.js +23 -23
  244. package/src/lib/server/assist-view-context.js.map +1 -1
  245. package/src/lib/server/billing-credits.d.ts +5 -0
  246. package/src/lib/server/billing-credits.js +8 -2
  247. package/src/lib/server/billing-credits.js.map +1 -1
  248. package/src/lib/server/billing-overage.js +2 -2
  249. package/src/lib/server/billing-overage.js.map +1 -1
  250. package/src/lib/server.d.ts +1 -0
  251. package/src/lib/server.js.map +1 -1
  252. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  253. package/src/lib/testing/stand-in-automation-index.js +102 -0
  254. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  255. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  256. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  257. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  258. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  259. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  260. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  261. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  262. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  263. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  264. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  265. package/src/lib/testing/stand-in-product-index.js +105 -0
  266. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  267. package/src/lib/tools/ai-component-tool.js +2 -2
  268. package/src/lib/tools/ai-component-tool.js.map +1 -1
  269. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  270. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  271. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  272. package/src/lib/tools/ai-seo-tool.js +7 -38
  273. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  274. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  275. package/src/lib/tools/ai-workflow-tool.js +1 -1
  276. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  277. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  278. package/src/lib/usage/ai-allotment-alerts.js +39 -15
  279. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  280. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  281. package/src/lib/usage/ai-quota-checks.js +69 -0
  282. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  283. package/src/lib/usage/ai-usage-alerts.js +22 -10
  284. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  285. package/src/lib/usage/ai-usage-by-user.js +1 -1
  286. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  287. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  288. package/src/lib/usage/assist-ceiling.js +4 -4
  289. package/src/lib/usage/assist-ceiling.js.map +1 -1
  290. package/src/lib/usage/assist-credits.d.ts +494 -0
  291. package/src/lib/usage/assist-credits.js +524 -0
  292. package/src/lib/usage/assist-credits.js.map +1 -0
  293. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  294. package/src/lib/usage/assist-free-taste.js +3 -2
  295. package/src/lib/usage/assist-free-taste.js.map +1 -1
  296. package/src/lib/usage/assist-signal-mining.js +1 -1
  297. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  298. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  299. package/src/lib/usage/assist-spend-guards.js +182 -0
  300. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  301. package/src/lib/usage/assist-usage.d.ts +1 -1
  302. package/src/lib/usage/assist-usage.js +1 -1
  303. package/src/lib/usage/assist-usage.js.map +1 -1
  304. package/src/lib/usage/staff-org-ai.js +1 -1
  305. package/src/lib/usage/staff-org-ai.js.map +1 -1
  306. package/src/lib/usage-axes.d.ts +49 -0
  307. package/src/lib/usage-axes.js +108 -0
  308. package/src/lib/usage-axes.js.map +1 -0
  309. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  310. package/src/lib/runtime/seo-page-facts.js +0 -150
  311. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page, and every other kind counts what a scaffold would. The figure the\n * proposal shows beside Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n return aiPlanCreditEstimate(plan, kind === 'page' ? { creates: AI_PAGE_CREATE_KINDS } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
@@ -72,6 +72,16 @@ export declare function aiBindingTokensIn(value: string): string[];
72
72
  export declare function isAiAddressToken(token: string): boolean;
73
73
  /** The tokens of a subject a link or media prop may hold. */
74
74
  export declare function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[];
75
+ /**
76
+ * Whether a token is filled for a machine and never read on the page: a URL
77
+ * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
78
+ * or the ISO timestamp a Video element's publication date takes
79
+ * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown
80
+ * one sees hyphens and digits (AGL-3143 §16).
81
+ */
82
+ export declare function isAiUnreadToken(token: string): boolean;
83
+ /** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */
84
+ export declare function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[];
75
85
  /** What a template job reads from its inputs. */
76
86
  export interface AiTemplateJobInputs {
77
87
  subject: AiTemplateSubject;
@@ -68,7 +68,7 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
68
68
  'collectionRelated'
69
69
  ],
70
70
  defaultName: 'Entry page template',
71
- appliedIn: 'the collection’s entry template screen, under Template screens in Content'
71
+ appliedIn: 'the collection’s entry template page, under Template pages in Content'
72
72
  },
73
73
  product: {
74
74
  subject: 'product',
@@ -91,7 +91,7 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
91
91
  'contentAuthorProfile'
92
92
  ],
93
93
  defaultName: 'Author page template',
94
- appliedIn: 'the author page screen on the Authors tab'
94
+ appliedIn: 'the author page on the Authors tab'
95
95
  }
96
96
  };
97
97
  /** Every `{{…}}` token a string holds, spelled without inner whitespace. */ export function aiBindingTokensIn(value) {
@@ -111,6 +111,20 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
111
111
  /** The tokens of a subject a link or media prop may hold. */ export function aiTemplateAddressTokens(definition) {
112
112
  return definition.tokens.map((entry)=>entry.token).filter(isAiAddressToken);
113
113
  }
114
+ /**
115
+ * Whether a token is filled for a machine and never read on the page: a URL
116
+ * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
117
+ * or the ISO timestamp a Video element's publication date takes
118
+ * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown
119
+ * one sees hyphens and digits (AGL-3143 §16).
120
+ */ export function isAiUnreadToken(token) {
121
+ var _token_replace_split_pop;
122
+ const field = (_token_replace_split_pop = token.replace(/[{}\s]/g, '').split('.').pop()) != null ? _token_replace_split_pop : '';
123
+ return /^(?:slug|publishedAt)$|Slug$/.test(field);
124
+ }
125
+ /** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */ export function aiTemplateShownTokens(definition) {
126
+ return definition.tokens.map((entry)=>entry.token).filter((token)=>!isAiUnreadToken(token));
127
+ }
114
128
  const RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/;
115
129
  /** The inputs of a template job, or a sentence naming what is missing. */ export function parseAiTemplateJobInputs(inputs) {
116
130
  var _ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template screen, under Template screens in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page screen on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,wBAAwB,EACxBC,mBAAmB,QAEd,+CAA8C;AAuBrD,OAAO,MAAMC,uBAAqD;IAAC;IAAS;IAAW;CAAS,CAAA;AAEhG;;;;;CAKC,GACD,OAAO,MAAMC,6BAAkE;IAC7E;QAAEC,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QAAEF,OAAO;QAAqBC,OAAO;QAASC,aAAa;IAA6B;IACxF;QAAEF,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAA6B;CACvF,CAAA;AAED,gFAAgF,GAChF,MAAMC,oBAAoBP,yBAAyBQ,MAAM,CAAC,CAACC,QACzDA,MAAML,KAAK,CAACM,UAAU,CAAC;AAuBzB,OAAO,MAAMC,kCAET;IACFF,OAAO;QACLG,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAId;eAAwBD;SAAyB;QAC7DgB,QAAQ;YAAC;YAAuB;YAAuB;YAAyB;SAAoB;QACpGC,aAAa;QACbC,WAAW;IACb;IACAC,SAAS;QACPP,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQZ;QACRa,QAAQ,EAAE;QACVC,aAAa;QACbC,WAAW;IACb;IACAE,QAAQ;QACNR,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAIhB;eAAyBQ;SAAkB;QACvDS,QAAQ;YAAC;SAAuB;QAChCC,aAAa;QACbC,WAAW;IACb;AACF,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASG,kBAAkBC,KAAa;IAC7C,OAAO;WAAIA,MAAMC,QAAQ,CAAC;KAA6B,CAACC,GAAG,CAAC,CAACC,QAAU,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiBtB,KAAa;QAC9BA;IAAd,MAAMuB,SAAQvB,2BAAAA,MAAMwB,OAAO,CAAC,gBAAgB,IAAIC,KAAK,CAAC,KAAKC,GAAG,cAAhD1B,2BAAsD;IACpE,OAAO,uCAAuC2B,IAAI,CAACJ;AACrD;AAEA,2DAA2D,GAC3D,OAAO,SAASK,wBAAwBC,UAAuC;IAC7E,OAAOA,WAAWlB,MAAM,CAACS,GAAG,CAAC,CAACf,QAAUA,MAAML,KAAK,EAAEI,MAAM,CAACkB;AAC9D;AASA,MAAMQ,YAAY;AAElB,wEAAwE,GACxE,OAAO,SAASC,yBACdC,MAA4D;;IAE5D,MAAMxB,UAAUyB,eAAOD,0BAAAA,MAAQ,CAAC,UAAU,mBAAI;IAC9C,IAAI,CAAC,AAAClC,qBAA2CoC,QAAQ,CAAC1B,UAAU;QAClE,OAAO,CAAC,wDAAwD,EAAEV,qBAAqBqC,IAAI,CAAC,OAAO;IACrG;IACA,IAAI3B,YAAY,SAAS,OAAO;QAAEA,SAASA;QAA8B4B,cAAc;IAAK;IAC5F,MAAMC,MAAML,0BAAAA,MAAQ,CAAC,eAAe;IACpC,MAAMI,eAAe,OAAOC,QAAQ,WAAWA,IAAIC,IAAI,KAAK;IAC5D,IAAI,CAACR,UAAUH,IAAI,CAACS,eAAe;QACjC,OAAO;IACT;IACA,OAAO;QAAE5B,SAAS;QAAS4B;IAAa;AAC1C"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/**\n * Whether a token is filled for a machine and never read on the page: a URL\n * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)\n * or the ISO timestamp a Video element's publication date takes\n * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown\n * one sees hyphens and digits (AGL-3143 §16).\n */\nexport function isAiUnreadToken(token: string): boolean {\n const field = token.replace(/[{}\\s]/g, '').split('.').pop() ?? ''\n return /^(?:slug|publishedAt)$|Slug$/.test(field)\n}\n\n/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */\nexport function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter((token) => !isAiUnreadToken(token))\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","isAiUnreadToken","aiTemplateShownTokens","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type HostThemeSource } from '@aglyn/aglyn/app-utils/marketplace-theme';
17
+ import { type HostThemeSource } from '@aglyn/aglyn/app-utils/site-theme';
18
18
  import type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types';
19
19
  import { type ThemeColorToken, type ThemeComponentOverrideLeaf, type ThemeEditorControlId } from '@aglyn/shared-ui-theme/util/theme-editor-fields';
20
20
  /**
@@ -14,7 +14,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { CONTRAST_AA, CONTRAST_AA_LARGE, contrastRatio } from "@aglyn/aglyn/app-utils/marketplace-theme";
17
+ */ import { CONTRAST_AA, CONTRAST_AA_LARGE, contrastRatio } from "@aglyn/aglyn/app-utils/site-theme";
18
18
  import { accessibleShade } from "@aglyn/shared-ui-theme/util/accessible-shade";
19
19
  import { inheritedThemeColor } from "@aglyn/shared-ui-theme/util/theme-editor-defaults";
20
20
  import { readDarkScheme, readFontFamily, readThemeColor, readToolbarHeight, resetComponentOverrides, SYSTEM_FONT_VALUE, THEME_COLOR_FIELDS, THEME_EDITOR_CONTROLS, THEME_EDITOR_MEDIA_QUERIES, THEME_EDITOR_SCHEMES, writeBorderRadius, writeComponentOverride, writeDarkScheme, writeFontFamily, writeSpacing, writeThemeColor, writeToolbarHeight } from "@aglyn/shared-ui-theme/util/theme-editor-fields";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' ||\n leaf['target'] === 'sx' ||\n leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' ||\n leaf['target'] === 'sx' ||\n leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
@@ -13,7 +13,7 @@
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 { assistProviderCostUsd, ASSIST_PROVIDER_COST_FIELD } from "@aglyn/aglyn/app-utils/assist-credits";
16
+ */ import { assistProviderCostUsd, ASSIST_PROVIDER_COST_FIELD } from "../usage/assist-credits.js";
17
17
  /**
18
18
  * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.
19
19
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-tokens.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n assistProviderCostUsd,\n ASSIST_PROVIDER_COST_FIELD,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport type { AiUsage } from '../providers/contract'\n\n/**\n * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.\n *\n * A provider reports four counts per request, and each is priced at its own\n * rate: prompt tokens sent uncached, prompt tokens read from the prompt\n * cache, prompt tokens written to it, and tokens generated. The org month\n * (per kind), the person's month and the job step keep the same four, so a\n * figure read off one means what it means on the others, and the cache hit\n * rate is the same division everywhere it is shown.\n *\n * Stored shapes:\n *\n * orgs/{orgId}/assistUsage/{YYYY-MM}\n * kinds: { [kind]: { requests, estCostUsd, providerCostUsd,\n * tokens: { input, cached, cacheWrite, output } } }\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * tokens: { input, cached, cacheWrite, output }\n *\n * The month document's own top-level `inputTokens`, `outputTokens`,\n * `cacheReadTokens` and `cacheWriteTokens` stay its totals; `kinds` splits\n * them by what the request was for.\n */\n\n/** The month document's per-kind map. */\nexport const AI_USAGE_MONTH_KINDS_FIELD = 'kinds'\n\n/** The token map, on a person's month and on each kind of the org's. */\nexport const AI_USAGE_TOKENS_FIELD = 'tokens'\n\nexport interface AiTokenTotals {\n /** Prompt tokens sent uncached, at the model's input rate. */\n input: number\n /** Prompt tokens read from the prompt cache, at its read rate. */\n cached: number\n /** Prompt tokens written to the prompt cache, at its write rate. */\n cacheWrite: number\n /** Tokens the model generated, thinking included. */\n output: number\n}\n\nconst count = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport function emptyAiTokenTotals(): AiTokenTotals {\n return { input: 0, cached: 0, cacheWrite: 0, output: 0 }\n}\n\n/** A request's usage, in the rollups' names. */\nexport function aiTokenTotalsFrom(usage: AiUsage): AiTokenTotals {\n return {\n input: count(usage.inputTokens),\n cached: count(usage.cacheReadTokens),\n cacheWrite: count(usage.cacheWriteTokens),\n output: count(usage.outputTokens),\n }\n}\n\nexport function addAiTokenTotals(a: AiTokenTotals, b: AiTokenTotals): AiTokenTotals {\n return {\n input: a.input + b.input,\n cached: a.cached + b.cached,\n cacheWrite: a.cacheWrite + b.cacheWrite,\n output: a.output + b.output,\n }\n}\n\n/** A stored token map read back, every absent or malformed count as zero. */\nexport function readAiTokenTotals(raw: unknown): AiTokenTotals {\n const record = isRecord(raw) ? raw : {}\n return {\n input: count(record['input']),\n cached: count(record['cached']),\n cacheWrite: count(record['cacheWrite']),\n output: count(record['output']),\n }\n}\n\n/**\n * The share of prompt tokens served from the cache: reads over everything the\n * prompt was billed as — sent, read and written.\n *\n * Writes sit in the denominator on purpose. A prefix that expires and is\n * rewritten every other request is the expensive failure, since a write costs\n * more than a fresh input token; a rate over reads and fresh input alone\n * reads that month as healthy. `null` when there are no prompt tokens at all,\n * which is nothing to divide rather than a 0% hit rate.\n */\nexport function aiCacheHitRate(\n tokens: Pick<AiTokenTotals, 'input' | 'cached' | 'cacheWrite'>,\n): number | null {\n const prompt = tokens.input + tokens.cached + tokens.cacheWrite\n return prompt > 0 ? tokens.cached / prompt : null\n}\n\n/** One kind of request in an org's month. */\nexport interface AiKindMonth {\n /** An `AiUsageKind`: `assist`, a besigner mode, or a job kind. */\n kind: string\n /** Metered model requests; a docs answer or a cache hit is not one. */\n requests: number\n /** What the kind DREW, at billed rates — the credits behind it. */\n estCostUsd: number\n /**\n * What the kind COST US (AGL-3015). Equal to `estCostUsd` on a kind served\n * only by models billed at their provider's list, and below it wherever a\n * marked-up model served. A month written before the split answers the\n * billed figure here, which over-reads our bill rather than under-reads it.\n */\n providerCostUsd: number\n tokens: AiTokenTotals\n}\n\n/**\n * The month document's `kinds` map as rows, dearest first.\n *\n * Ordered on what each kind DREW, not on what it cost us: the reader asking\n * \"which kind is expensive\" is asking about the meter every other figure on\n * the card is denominated in, and the two orders differ only where a kind's\n * model mix differs from its neighbour's.\n */\nexport function readAiKindMonths(raw: unknown): AiKindMonth[] {\n if (!isRecord(raw)) return []\n const rows: AiKindMonth[] = []\n for (const [kind, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue\n const cost = Number(value['estCostUsd'] ?? 0)\n const billed = Number.isFinite(cost) && cost > 0 ? cost : 0\n rows.push({\n kind,\n requests: count(value['requests']),\n estCostUsd: billed,\n providerCostUsd: assistProviderCostUsd(\n billed,\n value[ASSIST_PROVIDER_COST_FIELD],\n ),\n tokens: readAiTokenTotals(value[AI_USAGE_TOKENS_FIELD]),\n })\n }\n return rows.sort(\n (a, b) => b.estCostUsd - a.estCostUsd || b.requests - a.requests || a.kind.localeCompare(b.kind),\n )\n}\n"],"names":["assistProviderCostUsd","ASSIST_PROVIDER_COST_FIELD","AI_USAGE_MONTH_KINDS_FIELD","AI_USAGE_TOKENS_FIELD","count","value","parsed","Number","isFinite","Math","floor","isRecord","Array","isArray","emptyAiTokenTotals","input","cached","cacheWrite","output","aiTokenTotalsFrom","usage","inputTokens","cacheReadTokens","cacheWriteTokens","outputTokens","addAiTokenTotals","a","b","readAiTokenTotals","raw","record","aiCacheHitRate","tokens","prompt","readAiKindMonths","rows","kind","Object","entries","cost","billed","push","requests","estCostUsd","providerCostUsd","sort","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,0BAA0B,QACrB,wCAAuC;AAG9C;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,uCAAuC,GACvC,OAAO,MAAMC,6BAA6B,QAAO;AAEjD,sEAAsE,GACtE,OAAO,MAAMC,wBAAwB,SAAQ;AAa7C,MAAMC,QAAQ,CAACC;IACb,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;IAC/B,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIG,KAAKC,KAAK,CAACJ,UAAU;AACtE;AAEA,MAAMK,WAAW,CAACN,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACO,MAAMC,OAAO,CAACR;AAEhE,OAAO,SAASS;IACd,OAAO;QAAEC,OAAO;QAAGC,QAAQ;QAAGC,YAAY;QAAGC,QAAQ;IAAE;AACzD;AAEA,8CAA8C,GAC9C,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAO;QACLL,OAAOX,MAAMgB,MAAMC,WAAW;QAC9BL,QAAQZ,MAAMgB,MAAME,eAAe;QACnCL,YAAYb,MAAMgB,MAAMG,gBAAgB;QACxCL,QAAQd,MAAMgB,MAAMI,YAAY;IAClC;AACF;AAEA,OAAO,SAASC,iBAAiBC,CAAgB,EAAEC,CAAgB;IACjE,OAAO;QACLZ,OAAOW,EAAEX,KAAK,GAAGY,EAAEZ,KAAK;QACxBC,QAAQU,EAAEV,MAAM,GAAGW,EAAEX,MAAM;QAC3BC,YAAYS,EAAET,UAAU,GAAGU,EAAEV,UAAU;QACvCC,QAAQQ,EAAER,MAAM,GAAGS,EAAET,MAAM;IAC7B;AACF;AAEA,2EAA2E,GAC3E,OAAO,SAASU,kBAAkBC,GAAY;IAC5C,MAAMC,SAASnB,SAASkB,OAAOA,MAAM,CAAC;IACtC,OAAO;QACLd,OAAOX,MAAM0B,MAAM,CAAC,QAAQ;QAC5Bd,QAAQZ,MAAM0B,MAAM,CAAC,SAAS;QAC9Bb,YAAYb,MAAM0B,MAAM,CAAC,aAAa;QACtCZ,QAAQd,MAAM0B,MAAM,CAAC,SAAS;IAChC;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,eACdC,MAA8D;IAE9D,MAAMC,SAASD,OAAOjB,KAAK,GAAGiB,OAAOhB,MAAM,GAAGgB,OAAOf,UAAU;IAC/D,OAAOgB,SAAS,IAAID,OAAOhB,MAAM,GAAGiB,SAAS;AAC/C;AAoBA;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBL,GAAY;IAC3C,IAAI,CAAClB,SAASkB,MAAM,OAAO,EAAE;IAC7B,MAAMM,OAAsB,EAAE;IAC9B,KAAK,MAAM,CAACC,MAAM/B,MAAM,IAAIgC,OAAOC,OAAO,CAACT,KAAM;YAE3BxB;QADpB,IAAI,CAACM,SAASN,QAAQ;QACtB,MAAMkC,OAAOhC,QAAOF,oBAAAA,KAAK,CAAC,aAAa,YAAnBA,oBAAuB;QAC3C,MAAMmC,SAASjC,OAAOC,QAAQ,CAAC+B,SAASA,OAAO,IAAIA,OAAO;QAC1DJ,KAAKM,IAAI,CAAC;YACRL;YACAM,UAAUtC,MAAMC,KAAK,CAAC,WAAW;YACjCsC,YAAYH;YACZI,iBAAiB5C,sBACfwC,QACAnC,KAAK,CAACJ,2BAA2B;YAEnC+B,QAAQJ,kBAAkBvB,KAAK,CAACF,sBAAsB;QACxD;IACF;IACA,OAAOgC,KAAKU,IAAI,CACd,CAACnB,GAAGC,IAAMA,EAAEgB,UAAU,GAAGjB,EAAEiB,UAAU,IAAIhB,EAAEe,QAAQ,GAAGhB,EAAEgB,QAAQ,IAAIhB,EAAEU,IAAI,CAACU,aAAa,CAACnB,EAAES,IAAI;AAEnG"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-tokens.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n assistProviderCostUsd,\n ASSIST_PROVIDER_COST_FIELD,\n} from '../usage/assist-credits'\nimport type { AiUsage } from '../providers/contract'\n\n/**\n * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.\n *\n * A provider reports four counts per request, and each is priced at its own\n * rate: prompt tokens sent uncached, prompt tokens read from the prompt\n * cache, prompt tokens written to it, and tokens generated. The org month\n * (per kind), the person's month and the job step keep the same four, so a\n * figure read off one means what it means on the others, and the cache hit\n * rate is the same division everywhere it is shown.\n *\n * Stored shapes:\n *\n * orgs/{orgId}/assistUsage/{YYYY-MM}\n * kinds: { [kind]: { requests, estCostUsd, providerCostUsd,\n * tokens: { input, cached, cacheWrite, output } } }\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * tokens: { input, cached, cacheWrite, output }\n *\n * The month document's own top-level `inputTokens`, `outputTokens`,\n * `cacheReadTokens` and `cacheWriteTokens` stay its totals; `kinds` splits\n * them by what the request was for.\n */\n\n/** The month document's per-kind map. */\nexport const AI_USAGE_MONTH_KINDS_FIELD = 'kinds'\n\n/** The token map, on a person's month and on each kind of the org's. */\nexport const AI_USAGE_TOKENS_FIELD = 'tokens'\n\nexport interface AiTokenTotals {\n /** Prompt tokens sent uncached, at the model's input rate. */\n input: number\n /** Prompt tokens read from the prompt cache, at its read rate. */\n cached: number\n /** Prompt tokens written to the prompt cache, at its write rate. */\n cacheWrite: number\n /** Tokens the model generated, thinking included. */\n output: number\n}\n\nconst count = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport function emptyAiTokenTotals(): AiTokenTotals {\n return { input: 0, cached: 0, cacheWrite: 0, output: 0 }\n}\n\n/** A request's usage, in the rollups' names. */\nexport function aiTokenTotalsFrom(usage: AiUsage): AiTokenTotals {\n return {\n input: count(usage.inputTokens),\n cached: count(usage.cacheReadTokens),\n cacheWrite: count(usage.cacheWriteTokens),\n output: count(usage.outputTokens),\n }\n}\n\nexport function addAiTokenTotals(a: AiTokenTotals, b: AiTokenTotals): AiTokenTotals {\n return {\n input: a.input + b.input,\n cached: a.cached + b.cached,\n cacheWrite: a.cacheWrite + b.cacheWrite,\n output: a.output + b.output,\n }\n}\n\n/** A stored token map read back, every absent or malformed count as zero. */\nexport function readAiTokenTotals(raw: unknown): AiTokenTotals {\n const record = isRecord(raw) ? raw : {}\n return {\n input: count(record['input']),\n cached: count(record['cached']),\n cacheWrite: count(record['cacheWrite']),\n output: count(record['output']),\n }\n}\n\n/**\n * The share of prompt tokens served from the cache: reads over everything the\n * prompt was billed as — sent, read and written.\n *\n * Writes sit in the denominator on purpose. A prefix that expires and is\n * rewritten every other request is the expensive failure, since a write costs\n * more than a fresh input token; a rate over reads and fresh input alone\n * reads that month as healthy. `null` when there are no prompt tokens at all,\n * which is nothing to divide rather than a 0% hit rate.\n */\nexport function aiCacheHitRate(\n tokens: Pick<AiTokenTotals, 'input' | 'cached' | 'cacheWrite'>,\n): number | null {\n const prompt = tokens.input + tokens.cached + tokens.cacheWrite\n return prompt > 0 ? tokens.cached / prompt : null\n}\n\n/** One kind of request in an org's month. */\nexport interface AiKindMonth {\n /** An `AiUsageKind`: `assist`, a besigner mode, or a job kind. */\n kind: string\n /** Metered model requests; a docs answer or a cache hit is not one. */\n requests: number\n /** What the kind DREW, at billed rates — the credits behind it. */\n estCostUsd: number\n /**\n * What the kind COST US (AGL-3015). Equal to `estCostUsd` on a kind served\n * only by models billed at their provider's list, and below it wherever a\n * marked-up model served. A month written before the split answers the\n * billed figure here, which over-reads our bill rather than under-reads it.\n */\n providerCostUsd: number\n tokens: AiTokenTotals\n}\n\n/**\n * The month document's `kinds` map as rows, dearest first.\n *\n * Ordered on what each kind DREW, not on what it cost us: the reader asking\n * \"which kind is expensive\" is asking about the meter every other figure on\n * the card is denominated in, and the two orders differ only where a kind's\n * model mix differs from its neighbour's.\n */\nexport function readAiKindMonths(raw: unknown): AiKindMonth[] {\n if (!isRecord(raw)) return []\n const rows: AiKindMonth[] = []\n for (const [kind, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue\n const cost = Number(value['estCostUsd'] ?? 0)\n const billed = Number.isFinite(cost) && cost > 0 ? cost : 0\n rows.push({\n kind,\n requests: count(value['requests']),\n estCostUsd: billed,\n providerCostUsd: assistProviderCostUsd(\n billed,\n value[ASSIST_PROVIDER_COST_FIELD],\n ),\n tokens: readAiTokenTotals(value[AI_USAGE_TOKENS_FIELD]),\n })\n }\n return rows.sort(\n (a, b) => b.estCostUsd - a.estCostUsd || b.requests - a.requests || a.kind.localeCompare(b.kind),\n )\n}\n"],"names":["assistProviderCostUsd","ASSIST_PROVIDER_COST_FIELD","AI_USAGE_MONTH_KINDS_FIELD","AI_USAGE_TOKENS_FIELD","count","value","parsed","Number","isFinite","Math","floor","isRecord","Array","isArray","emptyAiTokenTotals","input","cached","cacheWrite","output","aiTokenTotalsFrom","usage","inputTokens","cacheReadTokens","cacheWriteTokens","outputTokens","addAiTokenTotals","a","b","readAiTokenTotals","raw","record","aiCacheHitRate","tokens","prompt","readAiKindMonths","rows","kind","Object","entries","cost","billed","push","requests","estCostUsd","providerCostUsd","sort","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,0BAA0B,QACrB,6BAAyB;AAGhC;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,uCAAuC,GACvC,OAAO,MAAMC,6BAA6B,QAAO;AAEjD,sEAAsE,GACtE,OAAO,MAAMC,wBAAwB,SAAQ;AAa7C,MAAMC,QAAQ,CAACC;IACb,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;IAC/B,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIG,KAAKC,KAAK,CAACJ,UAAU;AACtE;AAEA,MAAMK,WAAW,CAACN,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACO,MAAMC,OAAO,CAACR;AAEhE,OAAO,SAASS;IACd,OAAO;QAAEC,OAAO;QAAGC,QAAQ;QAAGC,YAAY;QAAGC,QAAQ;IAAE;AACzD;AAEA,8CAA8C,GAC9C,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAO;QACLL,OAAOX,MAAMgB,MAAMC,WAAW;QAC9BL,QAAQZ,MAAMgB,MAAME,eAAe;QACnCL,YAAYb,MAAMgB,MAAMG,gBAAgB;QACxCL,QAAQd,MAAMgB,MAAMI,YAAY;IAClC;AACF;AAEA,OAAO,SAASC,iBAAiBC,CAAgB,EAAEC,CAAgB;IACjE,OAAO;QACLZ,OAAOW,EAAEX,KAAK,GAAGY,EAAEZ,KAAK;QACxBC,QAAQU,EAAEV,MAAM,GAAGW,EAAEX,MAAM;QAC3BC,YAAYS,EAAET,UAAU,GAAGU,EAAEV,UAAU;QACvCC,QAAQQ,EAAER,MAAM,GAAGS,EAAET,MAAM;IAC7B;AACF;AAEA,2EAA2E,GAC3E,OAAO,SAASU,kBAAkBC,GAAY;IAC5C,MAAMC,SAASnB,SAASkB,OAAOA,MAAM,CAAC;IACtC,OAAO;QACLd,OAAOX,MAAM0B,MAAM,CAAC,QAAQ;QAC5Bd,QAAQZ,MAAM0B,MAAM,CAAC,SAAS;QAC9Bb,YAAYb,MAAM0B,MAAM,CAAC,aAAa;QACtCZ,QAAQd,MAAM0B,MAAM,CAAC,SAAS;IAChC;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,eACdC,MAA8D;IAE9D,MAAMC,SAASD,OAAOjB,KAAK,GAAGiB,OAAOhB,MAAM,GAAGgB,OAAOf,UAAU;IAC/D,OAAOgB,SAAS,IAAID,OAAOhB,MAAM,GAAGiB,SAAS;AAC/C;AAoBA;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBL,GAAY;IAC3C,IAAI,CAAClB,SAASkB,MAAM,OAAO,EAAE;IAC7B,MAAMM,OAAsB,EAAE;IAC9B,KAAK,MAAM,CAACC,MAAM/B,MAAM,IAAIgC,OAAOC,OAAO,CAACT,KAAM;YAE3BxB;QADpB,IAAI,CAACM,SAASN,QAAQ;QACtB,MAAMkC,OAAOhC,QAAOF,oBAAAA,KAAK,CAAC,aAAa,YAAnBA,oBAAuB;QAC3C,MAAMmC,SAASjC,OAAOC,QAAQ,CAAC+B,SAASA,OAAO,IAAIA,OAAO;QAC1DJ,KAAKM,IAAI,CAAC;YACRL;YACAM,UAAUtC,MAAMC,KAAK,CAAC,WAAW;YACjCsC,YAAYH;YACZI,iBAAiB5C,sBACfwC,QACAnC,KAAK,CAACJ,2BAA2B;YAEnC+B,QAAQJ,kBAAkBvB,KAAK,CAACF,sBAAsB;QACxD;IACF;IACA,OAAOgC,KAAKU,IAAI,CACd,CAACnB,GAAGC,IAAMA,EAAEgB,UAAU,GAAGjB,EAAEiB,UAAU,IAAIhB,EAAEe,QAAQ,GAAGhB,EAAEgB,QAAQ,IAAIhB,EAAEU,IAAI,CAACU,aAAa,CAACnB,EAAES,IAAI;AAEnG"}
@@ -13,7 +13,7 @@
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 { assistCreditsFromUsd } from "@aglyn/aglyn/app-utils/assist-credits";
16
+ */ import { assistCreditsFromUsd } from "../usage/assist-credits.js";
17
17
  /**
18
18
  * PER-USER AI USAGE (AGL-2928): the pure half.
19
19
  *