@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
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { AiBuildPlan, AiBuildPlanCreate, AiBuildPlanCreateKind } from './ai-build-plan';
18
+ import type { AiJobKind } from './ai-jobs.types';
19
+ /**
20
+ * What a job that builds ONE record builds of the rest of its plan
21
+ * (AGL-3143 §15): a template, a layout, a component, a form or an email
22
+ * design.
23
+ *
24
+ * Each of those steps builds the first creation of its own kind and nothing
25
+ * else, so a plan that also created something left it unbuilt and the job
26
+ * still reported `Done`. Measured live on 2026-10-01 (job `2xD9Y7NayF`): a
27
+ * template plan the member confirmed read "Creates the component
28
+ * related-article-card", and no such component was ever made.
29
+ *
30
+ * Such a job now builds the other creations the way a page job does
31
+ * (AGL-3031): each before its own record, one a pass, by the step that builds
32
+ * that kind, and then its own record against what was built. It builds only
33
+ * what its own record can PLACE — a component instance or a saved form, on
34
+ * the trees that admit one — because a creation nothing places is the same
35
+ * broken promise one step later. A form's tree and an email's place neither,
36
+ * so those two build only their own. The plan step is told this list before it
37
+ * answers, and the plan rules refuse a creation outside it with the one
38
+ * re-ask every rule gets.
39
+ *
40
+ * Model only: this module imports nothing at runtime, so the plan step, the
41
+ * doors, the estimate and the proposal read one list.
42
+ */
43
+ export declare const AI_JOB_CREATE_KINDS: Readonly<Partial<Record<AiJobKind, readonly AiBuildPlanCreateKind[]>>>;
44
+ /** A job of each of those kinds as a refusal names it: "a template job". */
45
+ export declare const AI_JOB_CREATE_NOUNS: Readonly<Partial<Record<AiJobKind, string>>>;
46
+ /**
47
+ * The creations a job of `kind` builds before its own record: every creation
48
+ * the plan names that the kind builds, less the one the kind's step builds
49
+ * itself — the first of the kind's own — in plan order. Empty for a kind that
50
+ * builds only its own record.
51
+ */
52
+ export declare function aiJobOtherCreations(kind: AiJobKind, plan: Pick<AiBuildPlan, 'create'>): Array<{
53
+ creation: AiBuildPlanCreate;
54
+ index: number;
55
+ }>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * What a job that builds ONE record builds of the rest of its plan
18
+ * (AGL-3143 §15): a template, a layout, a component, a form or an email
19
+ * design.
20
+ *
21
+ * Each of those steps builds the first creation of its own kind and nothing
22
+ * else, so a plan that also created something left it unbuilt and the job
23
+ * still reported `Done`. Measured live on 2026-10-01 (job `2xD9Y7NayF`): a
24
+ * template plan the member confirmed read "Creates the component
25
+ * related-article-card", and no such component was ever made.
26
+ *
27
+ * Such a job now builds the other creations the way a page job does
28
+ * (AGL-3031): each before its own record, one a pass, by the step that builds
29
+ * that kind, and then its own record against what was built. It builds only
30
+ * what its own record can PLACE — a component instance or a saved form, on
31
+ * the trees that admit one — because a creation nothing places is the same
32
+ * broken promise one step later. A form's tree and an email's place neither,
33
+ * so those two build only their own. The plan step is told this list before it
34
+ * answers, and the plan rules refuse a creation outside it with the one
35
+ * re-ask every rule gets.
36
+ *
37
+ * Model only: this module imports nothing at runtime, so the plan step, the
38
+ * doors, the estimate and the proposal read one list.
39
+ */ export const AI_JOB_CREATE_KINDS = {
40
+ template: [
41
+ 'template',
42
+ 'form',
43
+ 'component'
44
+ ],
45
+ layout: [
46
+ 'layout',
47
+ 'form',
48
+ 'component'
49
+ ],
50
+ component: [
51
+ 'component',
52
+ 'form'
53
+ ],
54
+ form: [
55
+ 'form'
56
+ ],
57
+ email: [
58
+ 'email'
59
+ ]
60
+ };
61
+ /** A job of each of those kinds as a refusal names it: "a template job". */ export const AI_JOB_CREATE_NOUNS = {
62
+ template: 'a template job',
63
+ layout: 'a layout job',
64
+ component: 'a component job',
65
+ form: 'a form job',
66
+ email: 'an email job'
67
+ };
68
+ /**
69
+ * The creations a job of `kind` builds before its own record: every creation
70
+ * the plan names that the kind builds, less the one the kind's step builds
71
+ * itself — the first of the kind's own — in plan order. Empty for a kind that
72
+ * builds only its own record.
73
+ */ export function aiJobOtherCreations(kind, plan) {
74
+ const creates = AI_JOB_CREATE_KINDS[kind];
75
+ if (!creates) return [];
76
+ const own = plan.create.findIndex((entry)=>entry.kind === kind);
77
+ return plan.create.flatMap((creation, index)=>index !== own && creates.includes(creation.kind) ? [
78
+ {
79
+ creation,
80
+ index
81
+ }
82
+ ] : []);
83
+ }
84
+
85
+ //# sourceMappingURL=ai-job-creations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-creations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiBuildPlan, AiBuildPlanCreate, AiBuildPlanCreateKind } from './ai-build-plan'\nimport type { AiJobKind } from './ai-jobs.types'\n\n/**\n * What a job that builds ONE record builds of the rest of its plan\n * (AGL-3143 §15): a template, a layout, a component, a form or an email\n * design.\n *\n * Each of those steps builds the first creation of its own kind and nothing\n * else, so a plan that also created something left it unbuilt and the job\n * still reported `Done`. Measured live on 2026-10-01 (job `2xD9Y7NayF`): a\n * template plan the member confirmed read \"Creates the component\n * related-article-card\", and no such component was ever made.\n *\n * Such a job now builds the other creations the way a page job does\n * (AGL-3031): each before its own record, one a pass, by the step that builds\n * that kind, and then its own record against what was built. It builds only\n * what its own record can PLACE — a component instance or a saved form, on\n * the trees that admit one — because a creation nothing places is the same\n * broken promise one step later. A form's tree and an email's place neither,\n * so those two build only their own. The plan step is told this list before it\n * answers, and the plan rules refuse a creation outside it with the one\n * re-ask every rule gets.\n *\n * Model only: this module imports nothing at runtime, so the plan step, the\n * doors, the estimate and the proposal read one list.\n */\nexport const AI_JOB_CREATE_KINDS: Readonly<Partial<Record<AiJobKind, readonly AiBuildPlanCreateKind[]>>> = {\n template: ['template', 'form', 'component'],\n layout: ['layout', 'form', 'component'],\n component: ['component', 'form'],\n form: ['form'],\n email: ['email'],\n}\n\n/** A job of each of those kinds as a refusal names it: \"a template job\". */\nexport const AI_JOB_CREATE_NOUNS: Readonly<Partial<Record<AiJobKind, string>>> = {\n template: 'a template job',\n layout: 'a layout job',\n component: 'a component job',\n form: 'a form job',\n email: 'an email job',\n}\n\n/**\n * The creations a job of `kind` builds before its own record: every creation\n * the plan names that the kind builds, less the one the kind's step builds\n * itself — the first of the kind's own — in plan order. Empty for a kind that\n * builds only its own record.\n */\nexport function aiJobOtherCreations(\n kind: AiJobKind,\n plan: Pick<AiBuildPlan, 'create'>,\n): Array<{ creation: AiBuildPlanCreate; index: number }> {\n const creates = AI_JOB_CREATE_KINDS[kind]\n if (!creates) return []\n const own = plan.create.findIndex((entry) => entry.kind === kind)\n return plan.create.flatMap((creation, index) =>\n index !== own && creates.includes(creation.kind) ? [{ creation, index }] : [],\n )\n}\n"],"names":["AI_JOB_CREATE_KINDS","template","layout","component","form","email","AI_JOB_CREATE_NOUNS","aiJobOtherCreations","kind","plan","creates","own","create","findIndex","entry","flatMap","creation","index","includes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,MAAMA,sBAA8F;IACzGC,UAAU;QAAC;QAAY;QAAQ;KAAY;IAC3CC,QAAQ;QAAC;QAAU;QAAQ;KAAY;IACvCC,WAAW;QAAC;QAAa;KAAO;IAChCC,MAAM;QAAC;KAAO;IACdC,OAAO;QAAC;KAAQ;AAClB,EAAC;AAED,0EAA0E,GAC1E,OAAO,MAAMC,sBAAoE;IAC/EL,UAAU;IACVC,QAAQ;IACRC,WAAW;IACXC,MAAM;IACNC,OAAO;AACT,EAAC;AAED;;;;;CAKC,GACD,OAAO,SAASE,oBACdC,IAAe,EACfC,IAAiC;IAEjC,MAAMC,UAAUV,mBAAmB,CAACQ,KAAK;IACzC,IAAI,CAACE,SAAS,OAAO,EAAE;IACvB,MAAMC,MAAMF,KAAKG,MAAM,CAACC,SAAS,CAAC,CAACC,QAAUA,MAAMN,IAAI,KAAKA;IAC5D,OAAOC,KAAKG,MAAM,CAACG,OAAO,CAAC,CAACC,UAAUC,QACpCA,UAAUN,OAAOD,QAAQQ,QAAQ,CAACF,SAASR,IAAI,IAAI;YAAC;gBAAEQ;gBAAUC;YAAM;SAAE,GAAG,EAAE;AAEjF"}
@@ -87,6 +87,7 @@ export declare function aiPagePlanRefusal(plan: AiBuildPlan): string | null;
87
87
  * in a sentence a member reads when confirming it: what the workspace's plan
88
88
  * does not include, or what the site has no room left for. The plan step was
89
89
  * told what could be created, so this is the site changing between the plan
90
- * and its confirmation.
90
+ * and its confirmation. `noun` names what the job builds, for a template,
91
+ * layout, component, form or email job held the same way (AGL-3143 §15).
91
92
  */
92
- export declare function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[]): string | null;
93
+ export declare function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[], noun?: string): string | null;
@@ -148,12 +148,13 @@ const PAGE_TYPE_BY_ID = new Map(AI_PAGE_TYPES.map((type)=>[
148
148
  * in a sentence a member reads when confirming it: what the workspace's plan
149
149
  * does not include, or what the site has no room left for. The plan step was
150
150
  * told what could be created, so this is the site changing between the plan
151
- * and its confirmation.
152
- */ export function aiPageCreationRefusal(refused) {
151
+ * and its confirmation. `noun` names what the job builds, for a template,
152
+ * layout, component, form or email job held the same way (AGL-3143 §15).
153
+ */ export function aiPageCreationRefusal(refused, noun = 'page') {
153
154
  if (!refused.length) return null;
154
155
  const parts = refused.map(({ kind, name, reason })=>`the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}”, because ${reason}`);
155
156
  const listed = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join('; ')}; and ${parts[parts.length - 1]}`;
156
- return `This page cannot be built as planned: it creates ${listed}. Describe the page again.`;
157
+ return `This ${noun} cannot be built as planned: it creates ${listed}. Describe the ${noun} again.`;
157
158
  }
158
159
 
159
160
  //# sourceMappingURL=ai-page-job.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-page-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 type { AiPlanUncreatable } from './ai-plan-capabilities'\n\n/**\n * What a page job is (AGL-2907): the page types a brief may name, the inputs\n * the job doors admit, and the plans a page generator can build.\n *\n * A page job builds ONE screen. It places the site's components by id, binds\n * its forms by id, and renders inside a layout — and since AGL-3031 it first\n * builds the layout, forms and components its own plan creates, in the same\n * job, through the steps that own them, so a doctrine-correct plan is one it\n * finishes rather than one it sends the member away from. Anything else a\n * plan creates — a template, a theme change, a dataset, an email — is another\n * job's work, and such a plan is refused before anything is spent, with a\n * sentence naming what to make first.\n *\n * This module imports nothing at runtime, so the doors, the step, the console\n * dialog and the specs read one vocabulary.\n */\n\nexport type AiPageType =\n | 'landing'\n | 'about'\n | 'pricing'\n | 'contact'\n | 'blogIndex'\n | 'product'\n | 'service'\n | 'event'\n\nexport interface AiPageTypeDefinition {\n id: AiPageType\n /** The chip's label in the Describe dialog. */\n label: string\n /** What the page is for, as the plan and the generation are told. */\n purpose: string\n}\n\nexport const AI_PAGE_TYPES: readonly AiPageTypeDefinition[] = [\n {\n id: 'landing',\n label: 'Landing',\n purpose: 'a landing page that makes one offer and asks for one action',\n },\n {\n id: 'about',\n label: 'About',\n purpose: 'an about page: who the business is, what it stands for and who is behind it',\n },\n {\n id: 'pricing',\n label: 'Pricing',\n purpose: 'a pricing page: the plans or packages, what each includes and how to start',\n },\n {\n id: 'contact',\n label: 'Contact',\n purpose: 'a contact page: how to reach the business, and where and when',\n },\n {\n id: 'blogIndex',\n label: 'Blog index',\n purpose: 'a blog index that lists the posts of the site’s content collection',\n },\n {\n id: 'product',\n label: 'Product',\n purpose: 'a page about one product: what it is, who it is for and how to get it',\n },\n {\n id: 'service',\n label: 'Service',\n purpose: 'a page about one service: what it covers, how it works and how to book it',\n },\n {\n id: 'event',\n label: 'Event',\n purpose: 'an event page: what it is, when and where, and how to attend',\n },\n]\n\nconst PAGE_TYPE_BY_ID = new Map(AI_PAGE_TYPES.map((type) => [type.id, type]))\n\n/** The definition of a page type id, or `null` for anything else. */\nexport function aiPageTypeDefinition(id: unknown): AiPageTypeDefinition | null {\n return typeof id === 'string' ? (PAGE_TYPE_BY_ID.get(id as AiPageType) ?? null) : null\n}\n\nexport interface AiPageJobInputs {\n /** The type the member picked; `null` when they picked none. */\n pageType: AiPageType | null\n}\n\n/** A page job's inputs, or the sentence the door refuses them with. */\nexport function parseAiPageJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiPageJobInputs | string {\n const raw = inputs?.['pageType']\n if (raw === undefined || raw === null || raw === '') return { pageType: null }\n const definition = aiPageTypeDefinition(raw)\n return definition\n ? { pageType: definition.id }\n : `pageType must be one of ${AI_PAGE_TYPES.map((type) => type.id).join(', ')}`\n}\n\n/** A creation a plan names, as the refusal lists it. */\nexport interface AiPagePrerequisite {\n kind: AiBuildPlanCreateKind\n name: string\n}\n\n/**\n * What the plan asks to be created that a page job does not build, in the\n * order the plan lists it, each named once: every entry is something a\n * member makes first.\n */\nexport function aiPagePlanPrerequisites(plan: AiBuildPlan): AiPagePrerequisite[] {\n const seen = new Set<string>()\n const prerequisites: AiPagePrerequisite[] = []\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_PAGE_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 * The creations a page job builds itself from its own plan (AGL-3031): the\n * layout the page renders inside, and the forms and components it places.\n * Each is built before the page, as a draft, by the step that builds that\n * kind on its own. The plan step is told this list before it answers, and\n * the plan rules refuse a creation outside it with the one re-ask every rule\n * gets, so a plan that needs another never reaches a member's Confirm.\n */\nexport const AI_PAGE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = ['layout', 'form', 'component']\n\n/**\n * Why a page job cannot build a plan of this SHAPE — no page, several pages,\n * a page with no sections — in a sentence a member reads; `null` when the\n * shape is one it builds. The plan step holds a plan to it with a re-ask, and\n * the doors hold a confirmed plan to it again through `aiPagePlanRefusal`.\n */\nexport function aiPagePlanShapeRefusal(plan: AiBuildPlan): string | null {\n if (plan.screens.length === 0) {\n return 'This plan has no page to build. Describe the page again.'\n }\n if (plan.screens.length > 1) {\n return `This plan builds ${plan.screens.length} pages, and a page job builds one. Describe each page on its own.`\n }\n if (!plan.screens[0].sections.length) {\n return 'This plan’s page has no sections to build. Describe the page again.'\n }\n return null\n}\n\n/**\n * Why a page job cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can. A page job builds exactly one screen,\n * from what the site already has.\n */\nexport function aiPagePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiPagePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiPagePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This page needs what the site does not have yet. Create ${listed}, then describe the page again.`\n}\n\n/**\n * Why a page job cannot build the creations its plan names ON THIS SITE NOW,\n * in a sentence a member reads when confirming it: what the workspace's plan\n * does not include, or what the site has no room left for. The plan step was\n * told what could be created, so this is the site changing between the plan\n * and its confirmation.\n */\nexport function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[]): string | null {\n if (!refused.length) return null\n const parts = refused.map(\n ({ kind, name, reason }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}”, because ${reason}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join('; ')}; and ${parts[parts.length - 1]}`\n return `This page cannot be built as planned: it creates ${listed}. Describe the page again.`\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_TYPES","id","label","purpose","PAGE_TYPE_BY_ID","Map","map","type","aiPageTypeDefinition","get","parseAiPageJobInputs","inputs","raw","undefined","pageType","definition","join","aiPagePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","key","toLowerCase","has","push","entry","create","AI_PAGE_CREATE_KINDS","includes","aiPagePlanShapeRefusal","screens","length","sections","aiPagePlanRefusal","shape","parts","noun","where","listed","slice","aiPageCreationRefusal","refused","reason"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-page-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 type { AiPlanUncreatable } from './ai-plan-capabilities'\n\n/**\n * What a page job is (AGL-2907): the page types a brief may name, the inputs\n * the job doors admit, and the plans a page generator can build.\n *\n * A page job builds ONE screen. It places the site's components by id, binds\n * its forms by id, and renders inside a layout — and since AGL-3031 it first\n * builds the layout, forms and components its own plan creates, in the same\n * job, through the steps that own them, so a doctrine-correct plan is one it\n * finishes rather than one it sends the member away from. Anything else a\n * plan creates — a template, a theme change, a dataset, an email — is another\n * job's work, and such a plan is refused before anything is spent, with a\n * sentence naming what to make first.\n *\n * This module imports nothing at runtime, so the doors, the step, the console\n * dialog and the specs read one vocabulary.\n */\n\nexport type AiPageType =\n | 'landing'\n | 'about'\n | 'pricing'\n | 'contact'\n | 'blogIndex'\n | 'product'\n | 'service'\n | 'event'\n\nexport interface AiPageTypeDefinition {\n id: AiPageType\n /** The chip's label in the Describe dialog. */\n label: string\n /** What the page is for, as the plan and the generation are told. */\n purpose: string\n}\n\nexport const AI_PAGE_TYPES: readonly AiPageTypeDefinition[] = [\n {\n id: 'landing',\n label: 'Landing',\n purpose: 'a landing page that makes one offer and asks for one action',\n },\n {\n id: 'about',\n label: 'About',\n purpose: 'an about page: who the business is, what it stands for and who is behind it',\n },\n {\n id: 'pricing',\n label: 'Pricing',\n purpose: 'a pricing page: the plans or packages, what each includes and how to start',\n },\n {\n id: 'contact',\n label: 'Contact',\n purpose: 'a contact page: how to reach the business, and where and when',\n },\n {\n id: 'blogIndex',\n label: 'Blog index',\n purpose: 'a blog index that lists the posts of the site’s content collection',\n },\n {\n id: 'product',\n label: 'Product',\n purpose: 'a page about one product: what it is, who it is for and how to get it',\n },\n {\n id: 'service',\n label: 'Service',\n purpose: 'a page about one service: what it covers, how it works and how to book it',\n },\n {\n id: 'event',\n label: 'Event',\n purpose: 'an event page: what it is, when and where, and how to attend',\n },\n]\n\nconst PAGE_TYPE_BY_ID = new Map(AI_PAGE_TYPES.map((type) => [type.id, type]))\n\n/** The definition of a page type id, or `null` for anything else. */\nexport function aiPageTypeDefinition(id: unknown): AiPageTypeDefinition | null {\n return typeof id === 'string' ? (PAGE_TYPE_BY_ID.get(id as AiPageType) ?? null) : null\n}\n\nexport interface AiPageJobInputs {\n /** The type the member picked; `null` when they picked none. */\n pageType: AiPageType | null\n}\n\n/** A page job's inputs, or the sentence the door refuses them with. */\nexport function parseAiPageJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiPageJobInputs | string {\n const raw = inputs?.['pageType']\n if (raw === undefined || raw === null || raw === '') return { pageType: null }\n const definition = aiPageTypeDefinition(raw)\n return definition\n ? { pageType: definition.id }\n : `pageType must be one of ${AI_PAGE_TYPES.map((type) => type.id).join(', ')}`\n}\n\n/** A creation a plan names, as the refusal lists it. */\nexport interface AiPagePrerequisite {\n kind: AiBuildPlanCreateKind\n name: string\n}\n\n/**\n * What the plan asks to be created that a page job does not build, in the\n * order the plan lists it, each named once: every entry is something a\n * member makes first.\n */\nexport function aiPagePlanPrerequisites(plan: AiBuildPlan): AiPagePrerequisite[] {\n const seen = new Set<string>()\n const prerequisites: AiPagePrerequisite[] = []\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_PAGE_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 * The creations a page job builds itself from its own plan (AGL-3031): the\n * layout the page renders inside, and the forms and components it places.\n * Each is built before the page, as a draft, by the step that builds that\n * kind on its own. The plan step is told this list before it answers, and\n * the plan rules refuse a creation outside it with the one re-ask every rule\n * gets, so a plan that needs another never reaches a member's Confirm.\n */\nexport const AI_PAGE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = ['layout', 'form', 'component']\n\n/**\n * Why a page job cannot build a plan of this SHAPE — no page, several pages,\n * a page with no sections — in a sentence a member reads; `null` when the\n * shape is one it builds. The plan step holds a plan to it with a re-ask, and\n * the doors hold a confirmed plan to it again through `aiPagePlanRefusal`.\n */\nexport function aiPagePlanShapeRefusal(plan: AiBuildPlan): string | null {\n if (plan.screens.length === 0) {\n return 'This plan has no page to build. Describe the page again.'\n }\n if (plan.screens.length > 1) {\n return `This plan builds ${plan.screens.length} pages, and a page job builds one. Describe each page on its own.`\n }\n if (!plan.screens[0].sections.length) {\n return 'This plan’s page has no sections to build. Describe the page again.'\n }\n return null\n}\n\n/**\n * Why a page job cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can. A page job builds exactly one screen,\n * from what the site already has.\n */\nexport function aiPagePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiPagePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiPagePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This page needs what the site does not have yet. Create ${listed}, then describe the page again.`\n}\n\n/**\n * Why a page job cannot build the creations its plan names ON THIS SITE NOW,\n * in a sentence a member reads when confirming it: what the workspace's plan\n * does not include, or what the site has no room left for. The plan step was\n * told what could be created, so this is the site changing between the plan\n * and its confirmation. `noun` names what the job builds, for a template,\n * layout, component, form or email job held the same way (AGL-3143 §15).\n */\nexport function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[], noun = 'page'): string | null {\n if (!refused.length) return null\n const parts = refused.map(\n ({ kind, name, reason }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}”, because ${reason}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join('; ')}; and ${parts[parts.length - 1]}`\n return `This ${noun} cannot be built as planned: it creates ${listed}. Describe the ${noun} again.`\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_TYPES","id","label","purpose","PAGE_TYPE_BY_ID","Map","map","type","aiPageTypeDefinition","get","parseAiPageJobInputs","inputs","raw","undefined","pageType","definition","join","aiPagePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","key","toLowerCase","has","push","entry","create","AI_PAGE_CREATE_KINDS","includes","aiPagePlanShapeRefusal","screens","length","sections","aiPagePlanRefusal","shape","parts","noun","where","listed","slice","aiPageCreationRefusal","refused","reason"],"mappings":"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"}
@@ -14,6 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import type { SeoAuditReport } from '@aglyn/aglyn/app-utils/seo-audit';
18
+ import type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords';
17
19
  import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fields';
18
20
  import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
19
21
  /**
@@ -34,8 +36,9 @@ import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
34
36
  * ## The outputs of a job
35
37
  *
36
38
  * - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).
37
- * - `audit:report` — the site audit: every audited page, its findings and
38
- * its score, and which pages need generated fixes ({@link AiSeoAuditReport}).
39
+ * - `audit:report` — the site audit: the platform SEO check's report (every
40
+ * audited page, its findings and its score) and which pages need generated
41
+ * fixes ({@link AiSeoAuditReport}).
39
42
  * - `audit:site` — structured data for the site's entity and the agent
40
43
  * guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).
41
44
  * - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).
@@ -47,14 +50,6 @@ export declare const AI_SEO_TARGETS: readonly AiSeoTarget[];
47
50
  export declare function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null;
48
51
  /** Proposed values for one listing, by field. */
49
52
  export type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>;
50
- /** Whether, and where, a page already says a target keyword. */
51
- export interface AiSeoKeywordCoverage {
52
- keyword: string;
53
- inTitle: boolean;
54
- inDescription: boolean;
55
- inH1: boolean;
56
- inBody: boolean;
57
- }
58
53
  /** A proposal for one page's or one product's search listing. */
59
54
  export interface AiSeoFieldsProposal {
60
55
  kind: 'fields';
@@ -68,55 +63,22 @@ export interface AiSeoFieldsProposal {
68
63
  };
69
64
  values: AiSeoFieldValues;
70
65
  /** The target keywords the listing was asked to cover, and where it now does. */
71
- keywords: AiSeoKeywordCoverage[];
66
+ keywords: SeoKeywordCoverage[];
72
67
  /** Customer-safe lines worth knowing (a field left out, and why). */
73
68
  notes: string[];
74
69
  }
75
- export type AiSeoFindingCode = 'title-missing' | 'title-too-long' | 'title-duplicate' | 'description-missing' | 'description-too-long' | 'description-duplicate' | 'h1-missing' | 'h1-multiple' | 'h1-thin' | 'image-alt-missing' | 'orphan' | 'keyword-missing' | 'search-discouraged' | 'entity-incomplete' | 'llms-guidance-missing';
76
- export type AiSeoSeverity = 'high' | 'medium' | 'low';
77
- export interface AiSeoFinding {
78
- code: AiSeoFindingCode;
79
- severity: AiSeoSeverity;
80
- /** One customer-safe sentence. */
81
- message: string;
82
- /** The elements it is about: the images without a description, the extra headings. */
83
- nodeIds?: string[];
84
- /** The other pages it involves: a duplicate's twins. */
85
- screenIds?: string[];
86
- /** The target keyword, for `keyword-missing`. */
87
- keyword?: string;
88
- }
89
- /** One audited page. */
90
- export interface AiSeoPageReport {
91
- screenId: string;
92
- /** The public path, `/` for the home page. */
93
- path: string;
94
- name: string;
95
- /** The published version the page was audited on. */
96
- versionId: string | null;
97
- /** 100 with nothing found, less for each finding by its severity. */
98
- score: number;
99
- findings: AiSeoFinding[];
100
- keywords: AiSeoKeywordCoverage[];
101
- }
102
- /** The site audit: every audited page, what was found, and what still needs generating. */
103
- export interface AiSeoAuditReport {
70
+ /**
71
+ * The site audit a job records: the platform's SEO check, whose findings any
72
+ * site owner sees without this plugin, plus what the job owes on top of it.
73
+ */
74
+ export interface AiSeoAuditReport extends SeoAuditReport {
104
75
  kind: 'audit';
105
- pages: AiSeoPageReport[];
106
- /** Published pages past the audit's page cap, not audited. */
107
- skipped: number;
108
- /** Findings about the site rather than a page. */
109
- site: AiSeoFinding[];
110
76
  /** Whether the site-wide proposal (structured data, agent guidance) is owed. */
111
77
  siteProposal: boolean;
112
78
  /** Pages whose fixes need generated text, in the order the batches take them. */
113
79
  queue: string[];
114
80
  /** How many pages one batch of fixes carries. */
115
81
  batchSize: number;
116
- /** The average page score. */
117
- score: number;
118
- /** Customer-safe lines about the audit itself: a keyword line naming no page, say. */
119
- notes: string[];
120
82
  }
121
83
  /** A content fix: applied into a NEW version of the page, never the published one. */
122
84
  export type AiSeoContentFix =
@@ -182,8 +144,6 @@ export declare const AI_SEO_OUTPUT_IDS: {
182
144
  readonly fixes: (batch: number) => string;
183
145
  readonly fields: (kind: "screen" | "product", id: string | null) => string;
184
146
  };
185
- /** What each finding is called where it is listed. */
186
- export declare const AI_SEO_FINDING_LABELS: Record<AiSeoFindingCode, string>;
187
147
  /** A stored proposal, or `null` for anything that is not one. */
188
148
  export declare function readAiSeoProposal(value: unknown): AiSeoProposal | null;
189
149
  /** One site audit, put back together from a job's outputs. */
@@ -41,23 +41,6 @@ export const AI_SEO_TARGETS = [
41
41
  fixes: (batch)=>`audit:fixes:${batch}`,
42
42
  fields: (kind, id)=>`fields:${kind}:${id != null ? id : 'new'}`
43
43
  };
44
- /** What each finding is called where it is listed. */ export const AI_SEO_FINDING_LABELS = {
45
- 'title-missing': 'No search title',
46
- 'title-too-long': 'Title too long',
47
- 'title-duplicate': 'Title used on another page',
48
- 'description-missing': 'No search description',
49
- 'description-too-long': 'Description too long',
50
- 'description-duplicate': 'Description used on another page',
51
- 'h1-missing': 'No main heading',
52
- 'h1-multiple': 'More than one main heading',
53
- 'h1-thin': 'Main heading says little',
54
- 'image-alt-missing': 'Images without a description',
55
- orphan: 'No page links here',
56
- 'keyword-missing': 'Target keyword not covered',
57
- 'search-discouraged': 'Search engines are discouraged',
58
- 'entity-incomplete': 'Structured data incomplete',
59
- 'llms-guidance-missing': 'No guidance for AI agents'
60
- };
61
44
  const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
62
45
  const strings = (value)=>Array.isArray(value) ? value.filter((entry)=>typeof entry === 'string') : [];
63
46
  function readValues(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-seo.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 isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/**\n * SEO by AI (AGL-2910): the proposals an `seo` job produces, in the shape\n * both the job step and the console cards read. Pure data and readers — no\n * server import — so a card can type what it renders from the same module\n * the step writes it with.\n *\n * ## Proposals, never writes\n *\n * Every value here is a PROPOSAL. A page's search listing is served from the\n * screen document, so a job never writes it: the values are staged in the\n * page's SEO card and saved there. A site audit's content fixes — an image\n * description, a page's one `h1` — become a new unpublished version only\n * when a person applies them, and the site-wide proposals land in the SEO\n * form unsaved. Nothing a job produces changes what a published site serves.\n *\n * ## The outputs of a job\n *\n * - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).\n * - `audit:report` — the site audit: every audited page, its findings and\n * its score, and which pages need generated fixes ({@link AiSeoAuditReport}).\n * - `audit:site` — structured data for the site's entity and the agent\n * guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).\n * - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).\n */\n\n/** What an `seo` job is about, as its `inputs.target` names it. */\nexport type AiSeoTarget = 'screen' | 'product' | 'site'\n\nexport const AI_SEO_TARGETS: readonly AiSeoTarget[] = ['screen', 'product', 'site']\n\n/** What an `seo` job is about, or `null` when its inputs name nothing an SEO job writes. */\nexport function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null {\n const target = inputs?.['target']\n return typeof target === 'string' && (AI_SEO_TARGETS as readonly string[]).includes(target)\n ? (target as AiSeoTarget)\n : null\n}\n\n/** Proposed values for one listing, by field. */\nexport type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/** Whether, and where, a page already says a target keyword. */\nexport interface AiSeoKeywordCoverage {\n keyword: string\n inTitle: boolean\n inDescription: boolean\n inH1: boolean\n inBody: boolean\n}\n\n/** A proposal for one page's or one product's search listing. */\nexport interface AiSeoFieldsProposal {\n kind: 'fields'\n subject: {\n kind: 'screen' | 'product'\n /** `null` for a product not saved yet. */\n id: string | null\n name: string\n /** The page's public path, for a screen. */\n path?: string | null\n }\n values: AiSeoFieldValues\n /** The target keywords the listing was asked to cover, and where it now does. */\n keywords: AiSeoKeywordCoverage[]\n /** Customer-safe lines worth knowing (a field left out, and why). */\n notes: string[]\n}\n\nexport type AiSeoFindingCode =\n | 'title-missing'\n | 'title-too-long'\n | 'title-duplicate'\n | 'description-missing'\n | 'description-too-long'\n | 'description-duplicate'\n | 'h1-missing'\n | 'h1-multiple'\n | 'h1-thin'\n | 'image-alt-missing'\n | 'orphan'\n | 'keyword-missing'\n | 'search-discouraged'\n | 'entity-incomplete'\n | 'llms-guidance-missing'\n\nexport type AiSeoSeverity = 'high' | 'medium' | 'low'\n\nexport interface AiSeoFinding {\n code: AiSeoFindingCode\n severity: AiSeoSeverity\n /** One customer-safe sentence. */\n message: string\n /** The elements it is about: the images without a description, the extra headings. */\n nodeIds?: string[]\n /** The other pages it involves: a duplicate's twins. */\n screenIds?: string[]\n /** The target keyword, for `keyword-missing`. */\n keyword?: string\n}\n\n/** One audited page. */\nexport interface AiSeoPageReport {\n screenId: string\n /** The public path, `/` for the home page. */\n path: string\n name: string\n /** The published version the page was audited on. */\n versionId: string | null\n /** 100 with nothing found, less for each finding by its severity. */\n score: number\n findings: AiSeoFinding[]\n keywords: AiSeoKeywordCoverage[]\n}\n\n/** The site audit: every audited page, what was found, and what still needs generating. */\nexport interface AiSeoAuditReport {\n kind: 'audit'\n pages: AiSeoPageReport[]\n /** Published pages past the audit's page cap, not audited. */\n skipped: number\n /** Findings about the site rather than a page. */\n site: AiSeoFinding[]\n /** Whether the site-wide proposal (structured data, agent guidance) is owed. */\n siteProposal: boolean\n /** Pages whose fixes need generated text, in the order the batches take them. */\n queue: string[]\n /** How many pages one batch of fixes carries. */\n batchSize: number\n /** The average page score. */\n score: number\n /** Customer-safe lines about the audit itself: a keyword line naming no page, say. */\n notes: string[]\n}\n\n/** A content fix: applied into a NEW version of the page, never the published one. */\nexport type AiSeoContentFix =\n /** An image with no description gets one. */\n | { kind: 'image-alt'; nodeId: string; alt: string }\n /** An existing heading becomes the page's `h1`, with this text. */\n | { kind: 'h1-set'; nodeId: string; text: string }\n /** A page with no heading to promote gets a new `h1` at the top of its content. */\n | { kind: 'h1-insert'; text: string }\n /** An extra `h1` becomes an `h2`, so the page keeps one. */\n | { kind: 'h1-demote'; nodeId: string }\n\n/** Everything proposed for one page. */\nexport interface AiSeoPageFix {\n screenId: string\n /** Listing values, staged in the page's SEO card and saved there. */\n values: AiSeoFieldValues\n content: AiSeoContentFix[]\n /** Findings with no fix a job can make — an orphan needs a person's link. */\n guidance: string[]\n}\n\nexport interface AiSeoFixesBatch {\n kind: 'fixes'\n /** 1-based. */\n batch: number\n fixes: AiSeoPageFix[]\n notes: string[]\n}\n\n/** The site's structured-data fields a proposal may fill, by form field name. */\nexport const AI_SEO_ENTITY_FORM_FIELDS = [\n 'seo.entity.type',\n 'seo.entity.name',\n 'seo.entity.description',\n 'seo.entity.email',\n 'seo.entity.telephone',\n 'seo.entity.contactType',\n] as const\n\n/** The agent guidance fields `/llms.txt` leads with, by form field name. */\nexport const AI_SEO_AGENT_FORM_FIELDS = ['seo.agent.whenToUse', 'seo.agent.howToUse'] as const\n\nexport type AiSeoSiteFormField =\n | (typeof AI_SEO_ENTITY_FORM_FIELDS)[number]\n | (typeof AI_SEO_AGENT_FORM_FIELDS)[number]\n\nexport interface AiSeoSiteProposal {\n /** Values for the site SEO form, by field name; only what the site's pages support. */\n values: Partial<Record<AiSeoSiteFormField, string>>\n /** `/llms.txt` as it would read once the guidance is saved. */\n llmsPreview: string\n notes: string[]\n}\n\nexport interface AiSeoSiteBatch {\n kind: 'site'\n site: AiSeoSiteProposal\n}\n\nexport type AiSeoProposal =\n | AiSeoFieldsProposal\n | AiSeoAuditReport\n | AiSeoFixesBatch\n | AiSeoSiteBatch\n\n/** Output ids, one place. */\nexport const AI_SEO_OUTPUT_IDS = {\n report: 'audit:report',\n site: 'audit:site',\n fixes: (batch: number) => `audit:fixes:${batch}`,\n fields: (kind: 'screen' | 'product', id: string | null) => `fields:${kind}:${id ?? 'new'}`,\n} as const\n\n/** What each finding is called where it is listed. */\nexport const AI_SEO_FINDING_LABELS: Record<AiSeoFindingCode, string> = {\n 'title-missing': 'No search title',\n 'title-too-long': 'Title too long',\n 'title-duplicate': 'Title used on another page',\n 'description-missing': 'No search description',\n 'description-too-long': 'Description too long',\n 'description-duplicate': 'Description used on another page',\n 'h1-missing': 'No main heading',\n 'h1-multiple': 'More than one main heading',\n 'h1-thin': 'Main heading says little',\n 'image-alt-missing': 'Images without a description',\n orphan: 'No page links here',\n 'keyword-missing': 'Target keyword not covered',\n 'search-discouraged': 'Search engines are discouraged',\n 'entity-incomplete': 'Structured data incomplete',\n 'llms-guidance-missing': 'No guidance for AI agents',\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\nfunction readValues(value: unknown): AiSeoFieldValues {\n const out: AiSeoFieldValues = {}\n if (!isRecord(value)) return out\n for (const [key, entry] of Object.entries(value)) {\n if (isSeoListingFieldKey(key) && typeof entry === 'string') out[key] = entry\n }\n return out\n}\n\n/** A stored proposal, or `null` for anything that is not one. */\nexport function readAiSeoProposal(value: unknown): AiSeoProposal | null {\n if (!isRecord(value)) return null\n switch (value['kind']) {\n case 'fields': {\n const subject = isRecord(value['subject']) ? value['subject'] : null\n if (!subject || (subject['kind'] !== 'screen' && subject['kind'] !== 'product')) return null\n return {\n kind: 'fields',\n subject: {\n kind: subject['kind'],\n id: typeof subject['id'] === 'string' ? subject['id'] : null,\n name: String(subject['name'] ?? ''),\n path: typeof subject['path'] === 'string' ? subject['path'] : null,\n },\n values: readValues(value['values']),\n keywords: Array.isArray(value['keywords']) ? (value['keywords'] as AiSeoKeywordCoverage[]) : [],\n notes: strings(value['notes']),\n }\n }\n case 'audit':\n return Array.isArray(value['pages']) ? (value as unknown as AiSeoAuditReport) : null\n case 'fixes':\n return Array.isArray(value['fixes']) ? (value as unknown as AiSeoFixesBatch) : null\n case 'site':\n return isRecord(value['site']) ? (value as unknown as AiSeoSiteBatch) : null\n default:\n return null\n }\n}\n\n/** One site audit, put back together from a job's outputs. */\nexport interface AiSeoAuditView {\n report: AiSeoAuditReport\n /** Every page fix proposed so far, by screen id. */\n fixes: Record<string, AiSeoPageFix>\n site: AiSeoSiteProposal | null\n /** Batches recorded so far. */\n batches: number\n /** Whether every batch the report queued has been recorded. */\n complete: boolean\n}\n\n/** The audit a job's outputs describe, or `null` when they describe none. */\nexport function aiSeoAuditView(outputs: readonly AiJobOutput[]): AiSeoAuditView | null {\n let report: AiSeoAuditReport | null = null\n let site: AiSeoSiteProposal | null = null\n const fixes: Record<string, AiSeoPageFix> = {}\n let batches = 0\n for (const output of outputs) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (!proposal) continue\n if (proposal.kind === 'audit') report = proposal\n else if (proposal.kind === 'site') site = proposal.site\n else if (proposal.kind === 'fixes') {\n batches += 1\n for (const fix of proposal.fixes) fixes[fix.screenId] = fix\n }\n }\n if (!report) return null\n const owedBatches = Math.ceil(report.queue.length / Math.max(1, report.batchSize))\n return {\n report,\n fixes,\n site,\n batches,\n complete: batches >= owedBatches && (!report.siteProposal || site !== null),\n }\n}\n\n/** The listing proposal a job produced, when it produced one. */\nexport function aiSeoFieldsProposalOf(job: Pick<AiJobSummary, 'outputs'> | null): AiSeoFieldsProposal | null {\n for (const output of job?.outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (proposal?.kind === 'fields') return proposal\n }\n return null\n}\n\n/** How many content fixes, and how many staged listings, an audit view would apply. */\nexport function aiSeoApplyCounts(view: AiSeoAuditView): { pagesWithContent: number; pagesWithValues: number } {\n let pagesWithContent = 0\n let pagesWithValues = 0\n for (const fix of Object.values(view.fixes)) {\n if (fix.content.length) pagesWithContent += 1\n if (Object.keys(fix.values).length) pagesWithValues += 1\n }\n return { pagesWithContent, pagesWithValues }\n}\n"],"names":["isSeoListingFieldKey","AI_SEO_TARGETS","aiSeoJobTarget","inputs","target","includes","AI_SEO_ENTITY_FORM_FIELDS","AI_SEO_AGENT_FORM_FIELDS","AI_SEO_OUTPUT_IDS","report","site","fixes","batch","fields","kind","id","AI_SEO_FINDING_LABELS","orphan","isRecord","value","Array","isArray","strings","filter","entry","readValues","out","key","Object","entries","readAiSeoProposal","subject","name","String","path","values","keywords","notes","aiSeoAuditView","outputs","batches","output","resource","proposal","fix","screenId","owedBatches","Math","ceil","queue","length","max","batchSize","complete","siteProposal","aiSeoFieldsProposalOf","job","aiSeoApplyCounts","view","pagesWithContent","pagesWithValues","content","keys"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-seo.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SeoAuditReport } from '@aglyn/aglyn/app-utils/seo-audit'\nimport type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\n\n/**\n * SEO by AI (AGL-2910): the proposals an `seo` job produces, in the shape\n * both the job step and the console cards read. Pure data and readers — no\n * server import — so a card can type what it renders from the same module\n * the step writes it with.\n *\n * ## Proposals, never writes\n *\n * Every value here is a PROPOSAL. A page's search listing is served from the\n * screen document, so a job never writes it: the values are staged in the\n * page's SEO card and saved there. A site audit's content fixes — an image\n * description, a page's one `h1` — become a new unpublished version only\n * when a person applies them, and the site-wide proposals land in the SEO\n * form unsaved. Nothing a job produces changes what a published site serves.\n *\n * ## The outputs of a job\n *\n * - `fields:{screen|product}:{id}` — one listing's values ({@link AiSeoFieldsProposal}).\n * - `audit:report` — the site audit: the platform SEO check's report (every\n * audited page, its findings and its score) and which pages need generated\n * fixes ({@link AiSeoAuditReport}).\n * - `audit:site` — structured data for the site's entity and the agent\n * guidance `/llms.txt` leads with ({@link AiSeoSiteBatch}).\n * - `audit:fixes:{n}` — one batch of page fixes ({@link AiSeoFixesBatch}).\n */\n\n/** What an `seo` job is about, as its `inputs.target` names it. */\nexport type AiSeoTarget = 'screen' | 'product' | 'site'\n\nexport const AI_SEO_TARGETS: readonly AiSeoTarget[] = ['screen', 'product', 'site']\n\n/** What an `seo` job is about, or `null` when its inputs name nothing an SEO job writes. */\nexport function aiSeoJobTarget(inputs: Record<string, unknown> | null | undefined): AiSeoTarget | null {\n const target = inputs?.['target']\n return typeof target === 'string' && (AI_SEO_TARGETS as readonly string[]).includes(target)\n ? (target as AiSeoTarget)\n : null\n}\n\n/** Proposed values for one listing, by field. */\nexport type AiSeoFieldValues = Partial<Record<SeoListingFieldKey, string>>\n\n/** A proposal for one page's or one product's search listing. */\nexport interface AiSeoFieldsProposal {\n kind: 'fields'\n subject: {\n kind: 'screen' | 'product'\n /** `null` for a product not saved yet. */\n id: string | null\n name: string\n /** The page's public path, for a screen. */\n path?: string | null\n }\n values: AiSeoFieldValues\n /** The target keywords the listing was asked to cover, and where it now does. */\n keywords: SeoKeywordCoverage[]\n /** Customer-safe lines worth knowing (a field left out, and why). */\n notes: string[]\n}\n\n/**\n * The site audit a job records: the platform's SEO check, whose findings any\n * site owner sees without this plugin, plus what the job owes on top of it.\n */\nexport interface AiSeoAuditReport extends SeoAuditReport {\n kind: 'audit'\n /** Whether the site-wide proposal (structured data, agent guidance) is owed. */\n siteProposal: boolean\n /** Pages whose fixes need generated text, in the order the batches take them. */\n queue: string[]\n /** How many pages one batch of fixes carries. */\n batchSize: number\n}\n\n/** A content fix: applied into a NEW version of the page, never the published one. */\nexport type AiSeoContentFix =\n /** An image with no description gets one. */\n | { kind: 'image-alt'; nodeId: string; alt: string }\n /** An existing heading becomes the page's `h1`, with this text. */\n | { kind: 'h1-set'; nodeId: string; text: string }\n /** A page with no heading to promote gets a new `h1` at the top of its content. */\n | { kind: 'h1-insert'; text: string }\n /** An extra `h1` becomes an `h2`, so the page keeps one. */\n | { kind: 'h1-demote'; nodeId: string }\n\n/** Everything proposed for one page. */\nexport interface AiSeoPageFix {\n screenId: string\n /** Listing values, staged in the page's SEO card and saved there. */\n values: AiSeoFieldValues\n content: AiSeoContentFix[]\n /** Findings with no fix a job can make — an orphan needs a person's link. */\n guidance: string[]\n}\n\nexport interface AiSeoFixesBatch {\n kind: 'fixes'\n /** 1-based. */\n batch: number\n fixes: AiSeoPageFix[]\n notes: string[]\n}\n\n/** The site's structured-data fields a proposal may fill, by form field name. */\nexport const AI_SEO_ENTITY_FORM_FIELDS = [\n 'seo.entity.type',\n 'seo.entity.name',\n 'seo.entity.description',\n 'seo.entity.email',\n 'seo.entity.telephone',\n 'seo.entity.contactType',\n] as const\n\n/** The agent guidance fields `/llms.txt` leads with, by form field name. */\nexport const AI_SEO_AGENT_FORM_FIELDS = ['seo.agent.whenToUse', 'seo.agent.howToUse'] as const\n\nexport type AiSeoSiteFormField =\n | (typeof AI_SEO_ENTITY_FORM_FIELDS)[number]\n | (typeof AI_SEO_AGENT_FORM_FIELDS)[number]\n\nexport interface AiSeoSiteProposal {\n /** Values for the site SEO form, by field name; only what the site's pages support. */\n values: Partial<Record<AiSeoSiteFormField, string>>\n /** `/llms.txt` as it would read once the guidance is saved. */\n llmsPreview: string\n notes: string[]\n}\n\nexport interface AiSeoSiteBatch {\n kind: 'site'\n site: AiSeoSiteProposal\n}\n\nexport type AiSeoProposal =\n | AiSeoFieldsProposal\n | AiSeoAuditReport\n | AiSeoFixesBatch\n | AiSeoSiteBatch\n\n/** Output ids, one place. */\nexport const AI_SEO_OUTPUT_IDS = {\n report: 'audit:report',\n site: 'audit:site',\n fixes: (batch: number) => `audit:fixes:${batch}`,\n fields: (kind: 'screen' | 'product', id: string | null) => `fields:${kind}:${id ?? 'new'}`,\n} as const\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\nfunction readValues(value: unknown): AiSeoFieldValues {\n const out: AiSeoFieldValues = {}\n if (!isRecord(value)) return out\n for (const [key, entry] of Object.entries(value)) {\n if (isSeoListingFieldKey(key) && typeof entry === 'string') out[key] = entry\n }\n return out\n}\n\n/** A stored proposal, or `null` for anything that is not one. */\nexport function readAiSeoProposal(value: unknown): AiSeoProposal | null {\n if (!isRecord(value)) return null\n switch (value['kind']) {\n case 'fields': {\n const subject = isRecord(value['subject']) ? value['subject'] : null\n if (!subject || (subject['kind'] !== 'screen' && subject['kind'] !== 'product')) return null\n return {\n kind: 'fields',\n subject: {\n kind: subject['kind'],\n id: typeof subject['id'] === 'string' ? subject['id'] : null,\n name: String(subject['name'] ?? ''),\n path: typeof subject['path'] === 'string' ? subject['path'] : null,\n },\n values: readValues(value['values']),\n keywords: Array.isArray(value['keywords']) ? (value['keywords'] as SeoKeywordCoverage[]) : [],\n notes: strings(value['notes']),\n }\n }\n case 'audit':\n return Array.isArray(value['pages']) ? (value as unknown as AiSeoAuditReport) : null\n case 'fixes':\n return Array.isArray(value['fixes']) ? (value as unknown as AiSeoFixesBatch) : null\n case 'site':\n return isRecord(value['site']) ? (value as unknown as AiSeoSiteBatch) : null\n default:\n return null\n }\n}\n\n/** One site audit, put back together from a job's outputs. */\nexport interface AiSeoAuditView {\n report: AiSeoAuditReport\n /** Every page fix proposed so far, by screen id. */\n fixes: Record<string, AiSeoPageFix>\n site: AiSeoSiteProposal | null\n /** Batches recorded so far. */\n batches: number\n /** Whether every batch the report queued has been recorded. */\n complete: boolean\n}\n\n/** The audit a job's outputs describe, or `null` when they describe none. */\nexport function aiSeoAuditView(outputs: readonly AiJobOutput[]): AiSeoAuditView | null {\n let report: AiSeoAuditReport | null = null\n let site: AiSeoSiteProposal | null = null\n const fixes: Record<string, AiSeoPageFix> = {}\n let batches = 0\n for (const output of outputs) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (!proposal) continue\n if (proposal.kind === 'audit') report = proposal\n else if (proposal.kind === 'site') site = proposal.site\n else if (proposal.kind === 'fixes') {\n batches += 1\n for (const fix of proposal.fixes) fixes[fix.screenId] = fix\n }\n }\n if (!report) return null\n const owedBatches = Math.ceil(report.queue.length / Math.max(1, report.batchSize))\n return {\n report,\n fixes,\n site,\n batches,\n complete: batches >= owedBatches && (!report.siteProposal || site !== null),\n }\n}\n\n/** The listing proposal a job produced, when it produced one. */\nexport function aiSeoFieldsProposalOf(job: Pick<AiJobSummary, 'outputs'> | null): AiSeoFieldsProposal | null {\n for (const output of job?.outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = readAiSeoProposal(output.proposal)\n if (proposal?.kind === 'fields') return proposal\n }\n return null\n}\n\n/** How many content fixes, and how many staged listings, an audit view would apply. */\nexport function aiSeoApplyCounts(view: AiSeoAuditView): { pagesWithContent: number; pagesWithValues: number } {\n let pagesWithContent = 0\n let pagesWithValues = 0\n for (const fix of Object.values(view.fixes)) {\n if (fix.content.length) pagesWithContent += 1\n if (Object.keys(fix.values).length) pagesWithValues += 1\n }\n return { pagesWithContent, pagesWithValues }\n}\n"],"names":["isSeoListingFieldKey","AI_SEO_TARGETS","aiSeoJobTarget","inputs","target","includes","AI_SEO_ENTITY_FORM_FIELDS","AI_SEO_AGENT_FORM_FIELDS","AI_SEO_OUTPUT_IDS","report","site","fixes","batch","fields","kind","id","isRecord","value","Array","isArray","strings","filter","entry","readValues","out","key","Object","entries","readAiSeoProposal","subject","name","String","path","values","keywords","notes","aiSeoAuditView","outputs","batches","output","resource","proposal","fix","screenId","owedBatches","Math","ceil","queue","length","max","batchSize","complete","siteProposal","aiSeoFieldsProposalOf","job","aiSeoApplyCounts","view","pagesWithContent","pagesWithValues","content","keys"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,QAEf,4CAA2C;AAgClD,OAAO,MAAMC,iBAAyC;IAAC;IAAU;IAAW;CAAO,CAAA;AAEnF,0FAA0F,GAC1F,OAAO,SAASC,eAAeC,MAAkD;IAC/E,MAAMC,SAASD,0BAAAA,MAAQ,CAAC,SAAS;IACjC,OAAO,OAAOC,WAAW,YAAY,AAACH,eAAqCI,QAAQ,CAACD,UAC/EA,SACD;AACN;AAkEA,+EAA+E,GAC/E,OAAO,MAAME,4BAA4B;IACvC;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,0EAA0E,GAC1E,OAAO,MAAMC,2BAA2B;IAAC;IAAuB;CAAqB,CAAS;AAyB9F,2BAA2B,GAC3B,OAAO,MAAMC,oBAAoB;IAC/BC,QAAQ;IACRC,MAAM;IACNC,OAAO,CAACC,QAAkB,CAAC,YAAY,EAAEA,OAAO;IAChDC,QAAQ,CAACC,MAA4BC,KAAsB,CAAC,OAAO,EAAED,KAAK,CAAC,EAAEC,aAAAA,KAAM,OAAO;AAC5F,EAAU;AAEV,MAAMC,WAAW,CAACC,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AAEhE,MAAMG,UAAU,CAACH,QACfC,MAAMC,OAAO,CAACF,SAASA,MAAMI,MAAM,CAAC,CAACC,QAA2B,OAAOA,UAAU,YAAY,EAAE;AAEjG,SAASC,WAAWN,KAAc;IAChC,MAAMO,MAAwB,CAAC;IAC/B,IAAI,CAACR,SAASC,QAAQ,OAAOO;IAC7B,KAAK,MAAM,CAACC,KAAKH,MAAM,IAAII,OAAOC,OAAO,CAACV,OAAQ;QAChD,IAAIjB,qBAAqByB,QAAQ,OAAOH,UAAU,UAAUE,GAAG,CAACC,IAAI,GAAGH;IACzE;IACA,OAAOE;AACT;AAEA,+DAA+D,GAC/D,OAAO,SAASI,kBAAkBX,KAAc;IAC9C,IAAI,CAACD,SAASC,QAAQ,OAAO;IAC7B,OAAQA,KAAK,CAAC,OAAO;QACnB,KAAK;YAAU;oBAQIY;gBAPjB,MAAMA,UAAUb,SAASC,KAAK,CAAC,UAAU,IAAIA,KAAK,CAAC,UAAU,GAAG;gBAChE,IAAI,CAACY,WAAYA,OAAO,CAAC,OAAO,KAAK,YAAYA,OAAO,CAAC,OAAO,KAAK,WAAY,OAAO;gBACxF,OAAO;oBACLf,MAAM;oBACNe,SAAS;wBACPf,MAAMe,OAAO,CAAC,OAAO;wBACrBd,IAAI,OAAOc,OAAO,CAAC,KAAK,KAAK,WAAWA,OAAO,CAAC,KAAK,GAAG;wBACxDC,MAAMC,QAAOF,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB;wBAChCG,MAAM,OAAOH,OAAO,CAAC,OAAO,KAAK,WAAWA,OAAO,CAAC,OAAO,GAAG;oBAChE;oBACAI,QAAQV,WAAWN,KAAK,CAAC,SAAS;oBAClCiB,UAAUhB,MAAMC,OAAO,CAACF,KAAK,CAAC,WAAW,IAAKA,KAAK,CAAC,WAAW,GAA4B,EAAE;oBAC7FkB,OAAOf,QAAQH,KAAK,CAAC,QAAQ;gBAC/B;YACF;QACA,KAAK;YACH,OAAOC,MAAMC,OAAO,CAACF,KAAK,CAAC,QAAQ,IAAKA,QAAwC;QAClF,KAAK;YACH,OAAOC,MAAMC,OAAO,CAACF,KAAK,CAAC,QAAQ,IAAKA,QAAuC;QACjF,KAAK;YACH,OAAOD,SAASC,KAAK,CAAC,OAAO,IAAKA,QAAsC;QAC1E;YACE,OAAO;IACX;AACF;AAcA,2EAA2E,GAC3E,OAAO,SAASmB,eAAeC,OAA+B;IAC5D,IAAI5B,SAAkC;IACtC,IAAIC,OAAiC;IACrC,MAAMC,QAAsC,CAAC;IAC7C,IAAI2B,UAAU;IACd,KAAK,MAAMC,UAAUF,QAAS;QAC5B,IAAIE,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWb,kBAAkBW,OAAOE,QAAQ;QAClD,IAAI,CAACA,UAAU;QACf,IAAIA,SAAS3B,IAAI,KAAK,SAASL,SAASgC;aACnC,IAAIA,SAAS3B,IAAI,KAAK,QAAQJ,OAAO+B,SAAS/B,IAAI;aAClD,IAAI+B,SAAS3B,IAAI,KAAK,SAAS;YAClCwB,WAAW;YACX,KAAK,MAAMI,OAAOD,SAAS9B,KAAK,CAAEA,KAAK,CAAC+B,IAAIC,QAAQ,CAAC,GAAGD;QAC1D;IACF;IACA,IAAI,CAACjC,QAAQ,OAAO;IACpB,MAAMmC,cAAcC,KAAKC,IAAI,CAACrC,OAAOsC,KAAK,CAACC,MAAM,GAAGH,KAAKI,GAAG,CAAC,GAAGxC,OAAOyC,SAAS;IAChF,OAAO;QACLzC;QACAE;QACAD;QACA4B;QACAa,UAAUb,WAAWM,eAAgB,CAAA,CAACnC,OAAO2C,YAAY,IAAI1C,SAAS,IAAG;IAC3E;AACF;AAEA,+DAA+D,GAC/D,OAAO,SAAS2C,sBAAsBC,GAAyC;;IAC7E,KAAK,MAAMf,kBAAUe,uBAAAA,IAAKjB,OAAO,mBAAI,EAAE,CAAE;QACvC,IAAIE,OAAOC,QAAQ,KAAK,OAAO;QAC/B,MAAMC,WAAWb,kBAAkBW,OAAOE,QAAQ;QAClD,IAAIA,CAAAA,4BAAAA,SAAU3B,IAAI,MAAK,UAAU,OAAO2B;IAC1C;IACA,OAAO;AACT;AAEA,qFAAqF,GACrF,OAAO,SAASc,iBAAiBC,IAAoB;IACnD,IAAIC,mBAAmB;IACvB,IAAIC,kBAAkB;IACtB,KAAK,MAAMhB,OAAOhB,OAAOO,MAAM,CAACuB,KAAK7C,KAAK,EAAG;QAC3C,IAAI+B,IAAIiB,OAAO,CAACX,MAAM,EAAES,oBAAoB;QAC5C,IAAI/B,OAAOkC,IAAI,CAAClB,IAAIT,MAAM,EAAEe,MAAM,EAAEU,mBAAmB;IACzD;IACA,OAAO;QAAED;QAAkBC;IAAgB;AAC7C"}
@@ -74,7 +74,7 @@ export const AI_INVENTORY_KINDS = [
74
74
  forms: 'Forms (id \u00b7 name \u00b7 fields)',
75
75
  datasets: 'Datasets (id \u00b7 name \u00b7 fields)',
76
76
  collections: 'Content collections (id \u00b7 name \u00b7 slug)',
77
- screens: 'Screens (id \u00b7 name \u00b7 slug)'
77
+ screens: 'Pages (id \u00b7 name \u00b7 slug)'
78
78
  };
79
79
  /**
80
80
  * One record as a line, in the order its heading names the columns. The one
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Screens (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n return (inventory?.screens ?? [])\n .filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n .map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","filter","has","trim","replace","test","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
@@ -233,8 +233,10 @@ export declare function aiPlanCreditEstimate(plan: AiBuildPlan, options?: AiPlan
233
233
  /**
234
234
  * About what a job of this kind costs to build from its plan (AGL-3031): a
235
235
  * page job counts the layout, forms and components it builds before its
236
- * page, and every other kind counts what a scaffold would. The figure the
237
- * proposal shows beside Confirm, and the hold a confirmation takes.
236
+ * page; a template, layout, component, form or email job counts its own
237
+ * record and every creation it builds before it (AGL-3143 §15); and a
238
+ * scaffold counts what it builds. The figure the proposal shows beside
239
+ * Confirm, and the hold a confirmation takes.
238
240
  */
239
241
  export declare function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number;
240
242
  /**
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { AI_BUILD_PLAN_CREATION_NOUNS, aiPlanUndeclaredRefs } from "./ai-build-plan.js";
17
+ import { AI_JOB_CREATE_KINDS } from "./ai-job-creations.js";
17
18
  import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
18
19
  /**
19
20
  * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted
@@ -294,11 +295,14 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
294
295
  /**
295
296
  * About what a job of this kind costs to build from its plan (AGL-3031): a
296
297
  * page job counts the layout, forms and components it builds before its
297
- * page, and every other kind counts what a scaffold would. The figure the
298
- * proposal shows beside Confirm, and the hold a confirmation takes.
298
+ * page; a template, layout, component, form or email job counts its own
299
+ * record and every creation it builds before it (AGL-3143 §15); and a
300
+ * scaffold counts what it builds. The figure the proposal shows beside
301
+ * Confirm, and the hold a confirmation takes.
299
302
  */ export function aiJobPlanCreditEstimate(kind, plan) {
300
- return aiPlanCreditEstimate(plan, kind === 'page' ? {
301
- creates: AI_PAGE_CREATE_KINDS
303
+ const creates = kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind];
304
+ return aiPlanCreditEstimate(plan, creates ? {
305
+ creates
302
306
  } : {});
303
307
  }
304
308
  /**