@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
@@ -17,19 +17,20 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ import { hostCollectionKind } from "@aglyn/aglyn/app-utils/collection-kind";
18
18
  import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
19
19
  import { buildLlmsTxt } from "@aglyn/aglyn/app-utils/llms-txt";
20
- import { nodesReferenceScreen } from "@aglyn/aglyn/app-utils/screen-link-value";
21
20
  import { screenRoutePathToUrl } from "@aglyn/aglyn/app-utils/screen-route";
22
- import { isScreenIndexable, isSearchDiscouraged, statusPageScreenIds } from "@aglyn/aglyn/app-utils/search-indexing";
21
+ import { SEO_FINDING_LABELS } from "@aglyn/aglyn/app-utils/seo-audit";
22
+ import { seoKeywordCoverage, seoKeywordList } from "@aglyn/aglyn/app-utils/seo-keywords";
23
23
  import { SCREEN_SEO_LISTING_FIELDS, isSeoListingFieldKey } from "@aglyn/aglyn/app-utils/seo-listing-fields";
24
+ import { SEO_PAGE_TEXT_MAX_CHARS, seoPageFacts } from "@aglyn/aglyn/app-utils/seo-page-facts";
25
+ import { scanSeoSite, seoAuditSiteOf } from "@aglyn/aglyn/app-utils/seo-site-scan";
24
26
  import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
25
27
  import { getTemplateScreenIds } from "@aglyn/tenant-runtime/template-screens";
26
- import { AI_SEO_FINDING_LABELS, AI_SEO_OUTPUT_IDS, aiSeoAuditView, aiSeoJobTarget } from "../model/ai-seo.js";
28
+ import { AI_SEO_OUTPUT_IDS, aiSeoAuditView, aiSeoJobTarget } from "../model/ai-seo.js";
27
29
  import { aiModelForStep } from "../providers/routing.js";
28
- import { AI_SEO_AUDIT_MAX_PAGES, aiSeoAudit, aiSeoAuditBatches, aiSeoHeadingDemotions, aiSeoNormalizePath, parseAiSeoKeywordLines } from "../runtime/seo-audit.js";
30
+ import { aiSeoAudit, aiSeoAuditBatches, aiSeoHeadingDemotions } from "../runtime/seo-audit.js";
29
31
  import { runValidatedGeneration } from "../runtime/ai-doctrine.js";
30
32
  import { AI_SEO_FIELDS_MAX_TOKENS, generateSeoFields } from "../runtime/seo-fields.js";
31
- import { AI_SEO_PAGE_TEXT_MAX_CHARS, aiSeoPageFacts } from "../runtime/seo-page-facts.js";
32
- import { AI_SEO_FIXES_TOOL_NAME, AI_SEO_SITE_TOOL_NAME, aiSeoFixImages, aiSeoFixesTool, aiSeoKeywordCoverage, aiSeoKeywordList, aiSeoSiteTool, checkAiSeoFixes, checkAiSeoSite } from "../tools/ai-seo-tool.js";
33
+ import { AI_SEO_FIXES_TOOL_NAME, AI_SEO_SITE_TOOL_NAME, aiSeoFixImages, aiSeoFixesTool, aiSeoSiteTool, checkAiSeoFixes, checkAiSeoSite } from "../tools/ai-seo-tool.js";
33
34
  import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS, aiSeoGenerationMaxTokens } from "./ai-job-seo-budget.js";
34
35
  /**
35
36
  * The `seo` step (AGL-2910): search listings and a site audit, as proposals.
@@ -56,8 +57,9 @@ import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS, aiSeoGenerationMaxToke
56
57
  * ## An audit, a unit at a time
57
58
  *
58
59
  * The first pass reads the site — every published page's version and the
59
- * shared layouts, for the links between them — scores it without a model,
60
- * records the report, and runs the first unit of generated work. Each later
60
+ * shared layouts, for the links between them — and scores it without a
61
+ * model, through the platform's SEO check (the same findings the site's SEO
62
+ * section lists to every owner); it records the report, and runs the first unit of generated work. Each later
61
63
  * pass runs one more unit: the site-wide proposal, then the page fixes in
62
64
  * batches. Every pass is one reservation and one provider exchange, and it
63
65
  * asks the machine to continue until nothing is left, so a large site is a
@@ -66,7 +68,6 @@ import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS, aiSeoGenerationMaxToke
66
68
  export const AI_SEO_NO_PAGE_COPY = 'This page is no longer on the site.';
67
69
  export const AI_SEO_NO_TARGET_COPY = 'This SEO job does not say what to write about.';
68
70
  export const AI_SEO_NO_PRODUCT_COPY = 'Give the product a name before writing its listing.';
69
- /** Shared layouts an audit reads for the links in a site's navigation. */ export const AI_SEO_LAYOUT_SCAN_LIMIT = 20;
70
71
  /** How much of each page a batch of fixes carries. */ export const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
71
72
  /**
72
73
  * Output budgets, declared with the step's time (`ai-job-seo-budget.ts`,
@@ -74,7 +75,6 @@ export const AI_SEO_NO_PRODUCT_COPY = 'Give the product a name before writing it
74
75
  * heading and image descriptions at their limits — and the site proposal's are
75
76
  * measured in `ai-job-seo-step.spec.ts`.
76
77
  */ export { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS };
77
- /** Documents read at once. */ const READ_CHUNK = 10;
78
78
  /** Pages the site prompt and the llms.txt preview list. */ const SITE_PAGES_LISTED = 25;
79
79
  /** The fields a product's listing holds. */ const PRODUCT_LISTING_FIELDS = [
80
80
  'title',
@@ -196,7 +196,7 @@ function addSpent(a, b) {
196
196
  kind: 'fields',
197
197
  subject,
198
198
  values,
199
- keywords: aiSeoKeywordCoverage(context.keywords, {
199
+ keywords: seoKeywordCoverage(context.keywords, {
200
200
  title: values.title,
201
201
  description: values.description,
202
202
  h1: (_context_facts = context.facts) == null ? void 0 : (_context_facts_h1s_ = _context_facts.h1s[0]) == null ? void 0 : _context_facts_h1s_.text,
@@ -235,7 +235,7 @@ async function runScreenListing(context, host) {
235
235
  }
236
236
  const versionId = str((_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['versionId']) || str(screen.versionId);
237
237
  const version = await readVersionNodes(hostRef, screenId, versionId);
238
- const facts = aiSeoPageFacts(version == null ? void 0 : version.nodes, {
238
+ const facts = seoPageFacts(version == null ? void 0 : version.nodes, {
239
239
  rootId: version == null ? void 0 : version.rootId
240
240
  });
241
241
  const fields = aiSeoRequestedFields((_job_inputs2 = job.inputs) == null ? void 0 : _job_inputs2['fields'], SCREEN_SEO_LISTING_FIELDS);
@@ -247,7 +247,7 @@ async function runScreenListing(context, host) {
247
247
  const route = (_host_screens = host.screens) == null ? void 0 : _host_screens[screenId];
248
248
  const path = typeof route === 'string' ? screenRoutePathToUrl(route) : null;
249
249
  const name = str(screen.displayName) || path || 'Untitled page';
250
- const keywords = aiSeoKeywordList((_job_inputs3 = job.inputs) == null ? void 0 : _job_inputs3['keywords']);
250
+ const keywords = seoKeywordList((_job_inputs3 = job.inputs) == null ? void 0 : _job_inputs3['keywords']);
251
251
  const generation = await generateSeoFields(_extends({
252
252
  subject: {
253
253
  kind: 'screen',
@@ -299,7 +299,7 @@ async function runProductListing(context, host) {
299
299
  failure: AI_SEO_NO_PRODUCT_COPY
300
300
  });
301
301
  const fields = aiSeoRequestedFields((_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['fields'], PRODUCT_LISTING_FIELDS);
302
- const keywords = aiSeoKeywordList((_job_inputs2 = job.inputs) == null ? void 0 : _job_inputs2['keywords']);
302
+ const keywords = seoKeywordList((_job_inputs2 = job.inputs) == null ? void 0 : _job_inputs2['keywords']);
303
303
  const productId = str((_job_inputs3 = job.inputs) == null ? void 0 : _job_inputs3['productId']) || null;
304
304
  const generation = await generateSeoFields(_extends({
305
305
  subject: {
@@ -309,7 +309,7 @@ async function runProductListing(context, host) {
309
309
  brand: brandOf(host),
310
310
  // The product's own words, as its editor handed them over: the product
311
311
  // document is the commerce plugin's, and this step never reads it.
312
- text: str((_job_inputs4 = job.inputs) == null ? void 0 : _job_inputs4['text']).slice(0, AI_SEO_PAGE_TEXT_MAX_CHARS),
312
+ text: str((_job_inputs4 = job.inputs) == null ? void 0 : _job_inputs4['text']).slice(0, SEO_PAGE_TEXT_MAX_CHARS),
313
313
  fields,
314
314
  current: {
315
315
  title: str((_job_inputs5 = job.inputs) == null ? void 0 : _job_inputs5['currentTitle']),
@@ -333,88 +333,19 @@ async function runProductListing(context, host) {
333
333
  hasImage: false
334
334
  });
335
335
  }
336
- /**
337
- * Read the site the way its sitemap lists it: every screen the routing map
338
- * publishes, less the template screens, the status pages and every screen
339
- * that is not public — through the same shared predicates the tenant's
340
- * `sitemap.xml` and `/llms.txt` apply, so the audit covers exactly the pages
341
- * a crawler is handed. Then each page's published version, and the shared
342
- * layouts, for the links that decide which pages nothing points to.
343
- */ export async function scanAiSeoSite(firestore, hostId, host, rawKeywords) {
344
- var _host_screens;
345
- const hostRef = firestore.collection('hosts').doc(hostId);
346
- const routing = (_host_screens = host.screens) != null ? _host_screens : {};
347
- const [screenDocs, templateScreenIds, layoutDocs] = await Promise.all([
348
- hostRef.collection('screens').select('visibility', 'seo', 'description', 'displayName', 'versionId', 'deletedAt').limit(1000).get(),
349
- getTemplateScreenIds({
336
+ /* ------------------------------------------------------------------------ *
337
+ * The site audit
338
+ * ------------------------------------------------------------------------ */ /**
339
+ * Read the site for an audit: the platform SEO check's scan
340
+ * (`@aglyn/aglyn/app-utils/seo-site-scan`), which the site's SEO section
341
+ * reads through too, so a job audits exactly the pages the check lists.
342
+ */ async function scanSite(firestore, hostId, host, keywords) {
343
+ return scanSeoSite(firestore, hostId, host, {
344
+ keywords,
345
+ templateScreenIds: await getTemplateScreenIds({
350
346
  hostId
351
- }),
352
- hostRef.collection('layouts').select('versionId').limit(AI_SEO_LAYOUT_SCAN_LIMIT).get()
353
- ]);
354
- const screens = new Map(screenDocs.docs.map((doc)=>[
355
- doc.id,
356
- doc.data()
357
- ]));
358
- const excluded = new Set([
359
- ...statusPageScreenIds(host),
360
- ...templateScreenIds
361
- ]);
362
- const published = Object.entries(routing).filter(([screenId])=>!excluded.has(screenId)).filter(([screenId])=>{
363
- const screen = screens.get(screenId);
364
- return Boolean(screen) && !(screen == null ? void 0 : screen.deletedAt) && isScreenIndexable(screen);
365
- }).map(([screenId, route])=>({
366
- screenId,
367
- path: screenRoutePathToUrl(String(route))
368
- })).sort((a, b)=>a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
369
- const audited = published.slice(0, AI_SEO_AUDIT_MAX_PAGES);
370
- const versions = new Map();
371
- for(let start = 0; start < audited.length; start += READ_CHUNK){
372
- await Promise.all(audited.slice(start, start + READ_CHUNK).map(async ({ screenId })=>{
373
- var _screens_get;
374
- versions.set(screenId, await readVersionNodes(hostRef, screenId, str((_screens_get = screens.get(screenId)) == null ? void 0 : _screens_get.versionId)));
375
- }));
376
- }
377
- const layoutNodes = [];
378
- const layouts = layoutDocs.docs.filter((doc)=>str(doc.get('versionId')));
379
- for(let start = 0; start < layouts.length; start += READ_CHUNK){
380
- await Promise.all(layouts.slice(start, start + READ_CHUNK).map(async (doc)=>{
381
- const snapshot = await doc.ref.collection('versions').doc(str(doc.get('versionId'))).get();
382
- const nodes = snapshot.exists ? decodeStoredNodes(snapshot.get('nodes')) : null;
383
- if (nodes) layoutNodes.push(nodes);
384
- }));
385
- }
386
- const keywordsByPath = parseAiSeoKeywordLines(rawKeywords);
387
- const auditedPaths = new Set(audited.map((page)=>aiSeoNormalizePath(page.path)));
388
- const notes = Object.keys(keywordsByPath).filter((path)=>!auditedPaths.has(path)).map((path)=>`Keywords for ${path} were not used: no audited page is published at that address.`);
389
- const pages = audited.map(({ screenId, path })=>{
390
- var _screens_get, _versions_get, _screen_seo, _keywordsByPath_aiSeoNormalizePath, _ref;
391
- const screen = (_screens_get = screens.get(screenId)) != null ? _screens_get : {};
392
- const version = (_versions_get = versions.get(screenId)) != null ? _versions_get : null;
393
- const linkedFrom = layoutNodes.some((nodes)=>nodesReferenceScreen(nodes, screenId)) || audited.some((other)=>{
394
- var _ref;
395
- var _versions_get;
396
- return other.screenId !== screenId && nodesReferenceScreen((_ref = (_versions_get = versions.get(other.screenId)) == null ? void 0 : _versions_get.nodes) != null ? _ref : null, screenId);
397
- });
398
- return {
399
- screenId,
400
- path,
401
- name: str(screen.displayName) || path,
402
- versionId: str(screen.versionId) || null,
403
- seo: (_screen_seo = screen.seo) != null ? _screen_seo : {},
404
- description: str(screen.description),
405
- facts: aiSeoPageFacts(version == null ? void 0 : version.nodes, {
406
- rootId: version == null ? void 0 : version.rootId
407
- }),
408
- linkedFrom,
409
- keywords: (_keywordsByPath_aiSeoNormalizePath = keywordsByPath[aiSeoNormalizePath(path)]) != null ? _keywordsByPath_aiSeoNormalizePath : [],
410
- nodes: (_ref = version == null ? void 0 : version.nodes) != null ? _ref : null
411
- };
347
+ })
412
348
  });
413
- return {
414
- pages,
415
- skipped: Math.max(0, published.length - audited.length),
416
- notes
417
- };
418
349
  }
419
350
  /** The units an audit owes after the outputs recorded so far, in the order they run. */ export function aiSeoPendingUnits(report, outputs) {
420
351
  const done = new Set(outputs.map((output)=>output.id));
@@ -623,7 +554,7 @@ async function proposeSite(context, host, scan) {
623
554
  `### Page ${page.screenId}: ${page.name} (${page.path})`,
624
555
  `Found: ${[
625
556
  ...page.codes
626
- ].map((code)=>AI_SEO_FINDING_LABELS[code]).join('; ')}`,
557
+ ].map((code)=>SEO_FINDING_LABELS[code]).join('; ')}`,
627
558
  `Title now: ${str((_page_seo = page.seo) == null ? void 0 : _page_seo.title) || '(none)'}`,
628
559
  `Description now: ${str((_page_seo1 = page.seo) == null ? void 0 : _page_seo1.description) || '(none)'}`,
629
560
  `Main heading now: ${((_page_facts_h1s_ = page.facts.h1s[0]) == null ? void 0 : _page_facts_h1s_.text) || '(none)'}`
@@ -663,7 +594,7 @@ async function proposeFixes(context, report, unit) {
663
594
  codes: new Set(pageReport.findings.map((entry)=>entry.code)),
664
595
  keywords: pageReport.keywords.map((entry)=>entry.keyword),
665
596
  seo: (_screen_seo = screen.seo) != null ? _screen_seo : {},
666
- facts: aiSeoPageFacts(version == null ? void 0 : version.nodes, {
597
+ facts: seoPageFacts(version == null ? void 0 : version.nodes, {
667
598
  rootId: version == null ? void 0 : version.rootId
668
599
  })
669
600
  };
@@ -737,17 +668,13 @@ async function runSiteAuditPass(context, host) {
737
668
  if (recorded) {
738
669
  report = recorded.report;
739
670
  } else {
740
- var _ref, _ref1, _job_hostId;
741
- var _job_inputs1, _host_seo, _host_seo1;
742
- scan = await scanAiSeoSite(firestore, job.hostId, host, (_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['keywords']);
743
- report = aiSeoAudit(scan.pages, {
744
- discouraged: isSearchDiscouraged(host),
745
- entity: (_ref = (_host_seo = host.seo) == null ? void 0 : _host_seo.entity) != null ? _ref : {},
746
- agent: (_ref1 = (_host_seo1 = host.seo) == null ? void 0 : _host_seo1.agent) != null ? _ref1 : {}
747
- }, {
748
- skipped: scan.skipped
671
+ var _job_hostId;
672
+ var _job_inputs1;
673
+ scan = await scanSite(firestore, job.hostId, host, (_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['keywords']);
674
+ report = aiSeoAudit(scan.pages, seoAuditSiteOf(host), {
675
+ skipped: scan.skipped,
676
+ notes: scan.notes
749
677
  });
750
- report.notes.push(...scan.notes);
751
678
  const findings = report.pages.reduce((sum, page)=>sum + page.findings.length, report.site.length);
752
679
  outputs.push({
753
680
  resource: 'seo',
@@ -765,7 +692,7 @@ async function runSiteAuditPass(context, host) {
765
692
  outputs
766
693
  }, spent);
767
694
  const [unit] = units;
768
- const result = unit.kind === 'site' ? await proposeSite(context, host, scan != null ? scan : await scanAiSeoSite(firestore, job.hostId, host, (_job_inputs = job.inputs) == null ? void 0 : _job_inputs['keywords'])) : await proposeFixes(context, report, unit);
695
+ const result = unit.kind === 'site' ? await proposeSite(context, host, scan != null ? scan : await scanSite(firestore, job.hostId, host, (_job_inputs = job.inputs) == null ? void 0 : _job_inputs['keywords'])) : await proposeFixes(context, report, unit);
769
696
  outputs.push(result.output);
770
697
  spent = addSpent(spent, result.spent);
771
698
  return _extends({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-seo-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n buildLlmsTxt,\n type LlmsTxtCollection,\n type LlmsTxtPage,\n} from '@aglyn/aglyn/app-utils/llms-txt'\nimport { nodesReferenceScreen } from '@aglyn/aglyn/app-utils/screen-link-value'\nimport { screenRoutePathToUrl } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n isScreenIndexable,\n isSearchDiscouraged,\n statusPageScreenIds,\n} from '@aglyn/aglyn/app-utils/search-indexing'\nimport {\n SCREEN_SEO_LISTING_FIELDS,\n isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { getTemplateScreenIds } from '@aglyn/tenant-runtime/template-screens'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_SEO_FINDING_LABELS,\n AI_SEO_OUTPUT_IDS,\n aiSeoAuditView,\n aiSeoJobTarget,\n type AiSeoAuditReport,\n type AiSeoContentFix,\n type AiSeoFieldsProposal,\n type AiSeoFieldValues,\n type AiSeoPageFix,\n type AiSeoSiteFormField,\n type AiSeoSiteProposal,\n} from '../model/ai-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiSystemBlock, AiUsage } from '../runtime/ai-runtime'\nimport {\n AI_SEO_AUDIT_MAX_PAGES,\n aiSeoAudit,\n aiSeoAuditBatches,\n aiSeoHeadingDemotions,\n aiSeoNormalizePath,\n parseAiSeoKeywordLines,\n type AiSeoAuditPage,\n} from '../runtime/seo-audit'\nimport {\n runValidatedGeneration,\n type AiGenerationSpend,\n type AiValidatedGeneration,\n} from '../runtime/ai-doctrine'\nimport { AI_SEO_FIELDS_MAX_TOKENS, generateSeoFields } from '../runtime/seo-fields'\nimport {\n AI_SEO_PAGE_TEXT_MAX_CHARS,\n aiSeoPageFacts,\n type AiSeoPageFacts,\n} from '../runtime/seo-page-facts'\nimport {\n AI_SEO_FIXES_TOOL_NAME,\n AI_SEO_SITE_TOOL_NAME,\n aiSeoFixImages,\n aiSeoFixesTool,\n aiSeoKeywordCoverage,\n aiSeoKeywordList,\n aiSeoSiteTool,\n checkAiSeoFixes,\n checkAiSeoSite,\n type AiSeoBatchAnswer,\n type AiSeoBatchPage,\n} from '../tools/ai-seo-tool'\nimport {\n AI_SEO_FIXES_MAX_TOKENS,\n AI_SEO_SITE_MAX_TOKENS,\n aiSeoGenerationMaxTokens,\n} from './ai-job-seo-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The `seo` step (AGL-2910): search listings and a site audit, as proposals.\n *\n * `inputs.target` says what the job is about:\n *\n * - `screen` — one page's listing, written from the version the person is\n * looking at (`screenId`, `versionId`), for the page's SEO card;\n * - `product` — one product's listing, from the name and description the\n * product editor handed over (`name`, `text`), for the product editor;\n * - `site` — the audit of every published page the sitemap lists, with a\n * proposed fix per finding, structured data for the site's entity, and the\n * agent guidance `/llms.txt` leads with.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. A listing is served\n * straight from the screen document, the structured data and `/llms.txt`\n * from the host document, and a page's content from its published version,\n * so the values ride on the job's outputs and nothing else: a person puts\n * them in the page's SEO card or the site SEO form and saves them there, and\n * content fixes become a new version only through the apply door.\n *\n * ## An audit, a unit at a time\n *\n * The first pass reads the site — every published page's version and the\n * shared layouts, for the links between them — scores it without a model,\n * records the report, and runs the first unit of generated work. Each later\n * pass runs one more unit: the site-wide proposal, then the page fixes in\n * batches. Every pass is one reservation and one provider exchange, and it\n * asks the machine to continue until nothing is left, so a large site is a\n * few beats of work rather than one request that cannot finish.\n */\n\nexport const AI_SEO_NO_SITE_COPY = 'This SEO job names a site this workspace does not have.'\nexport const AI_SEO_NO_PAGE_COPY = 'This page is no longer on the site.'\nexport const AI_SEO_NO_TARGET_COPY = 'This SEO job does not say what to write about.'\nexport const AI_SEO_NO_PRODUCT_COPY = 'Give the product a name before writing its listing.'\n\n/** Shared layouts an audit reads for the links in a site's navigation. */\nexport const AI_SEO_LAYOUT_SCAN_LIMIT = 20\n\n/** How much of each page a batch of fixes carries. */\nexport const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700\n\n/**\n * Output budgets, declared with the step's time (`ai-job-seo-budget.ts`,\n * AGL-3035). A batch's largest answer — every page with a title, description,\n * heading and image descriptions at their limits — and the site proposal's are\n * measured in `ai-job-seo-step.spec.ts`.\n */\nexport { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS }\n\n/** Documents read at once. */\nconst READ_CHUNK = 10\n\n/** Pages the site prompt and the llms.txt preview list. */\nconst SITE_PAGES_LISTED = 25\n\n/** The fields a product's listing holds. */\nconst PRODUCT_LISTING_FIELDS: readonly SeoListingFieldKey[] = ['title', 'description']\n\nconst ZERO_USAGE: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\ntype DocumentReference = FirebaseFirestore.DocumentReference\ntype Spent = Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n\n/** The host document fields this step reads. */\nexport interface SeoHostDocument {\n orgId?: string\n displayName?: string\n subdomain?: string | null\n cname?: string | null\n screens?: Record<string, string>\n errorScreens?: Record<string, string>\n notFoundScreenId?: string | null\n seo?: {\n title?: string\n description?: string\n discourageSearchEngines?: boolean\n entity?: {\n type?: unknown\n name?: string\n description?: string\n email?: string\n telephone?: string\n contactType?: string\n }\n agent?: { whenToUse?: string; howToUse?: string }\n }\n}\n\n/** The screen document fields this step reads. */\ninterface SeoScreenDocument {\n displayName?: string\n description?: string\n versionId?: string\n visibility?: unknown\n deletedAt?: unknown\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n image?: string\n imageAlt?: string\n }\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst round6 = (value: number): number => Math.round(value * 1_000_000) / 1_000_000\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.('job.seo') ?? aiModelForStep('job.seo')\n}\n\nfunction spentNothing(model: string): Spent {\n return { usage: ZERO_USAGE, estCostUsd: 0, model, stopReason: null }\n}\n\nfunction spentBy(generation: AiGenerationSpend): Spent {\n return {\n usage: generation.usage,\n estCostUsd: generation.estCostUsd,\n model: generation.model,\n stopReason: generation.stopReason,\n }\n}\n\nfunction addSpent(a: Spent, b: Spent): Spent {\n return {\n usage: {\n inputTokens: a.usage.inputTokens + b.usage.inputTokens,\n outputTokens: a.usage.outputTokens + b.usage.outputTokens,\n cacheReadTokens: a.usage.cacheReadTokens + b.usage.cacheReadTokens,\n cacheWriteTokens: a.usage.cacheWriteTokens + b.usage.cacheWriteTokens,\n },\n estCostUsd: round6(a.estCostUsd + b.estCostUsd),\n model: b.model || a.model,\n stopReason: b.stopReason ?? a.stopReason,\n }\n}\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/**\n * The fields a listing job writes: the ones the editor named (`inputs.fields`,\n * comma-separated) that the target's editor holds, or all of those.\n */\nexport function aiSeoRequestedFields(\n raw: unknown,\n allowed: readonly SeoListingFieldKey[],\n): SeoListingFieldKey[] {\n const named = String(raw ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter(isSeoListingFieldKey)\n const wanted = new Set<SeoListingFieldKey>(named.length ? named : allowed)\n return allowed.filter((key) => wanted.has(key))\n}\n\n/** The site's name, as its visitors read it. */\nfunction brandOf(host: SeoHostDocument): string {\n return str(host.seo?.title) || str(host.displayName) || str(host.subdomain)\n}\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadSite(\n firestore: Firestore,\n job: AiJob,\n): Promise<{ host: SeoHostDocument } | { failure: string }> {\n if (!job.hostId) return { failure: AI_SEO_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as SeoHostDocument) : null\n // The route checks the caller's membership of the org the job is metered\n // against, not that the site belongs to it. A site of another org is\n // refused here, before anything about it reaches a prompt.\n if (!host || host.orgId !== job.orgId) return { failure: AI_SEO_NO_SITE_COPY }\n return { host }\n}\n\n/** A version's node map and root, or `null` when it cannot be read. */\nasync function readVersionNodes(\n hostRef: DocumentReference,\n screenId: string,\n versionId: string,\n): Promise<{ nodes: Record<string, unknown>; rootId?: string } | null> {\n if (!versionId) return null\n const snapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .collection('versions')\n .doc(versionId)\n .get()\n if (!snapshot.exists) return null\n const nodes = decodeStoredNodes<Record<string, unknown>>(snapshot.get('nodes'))\n if (!nodes) return null\n const rootId = snapshot.get('rootId')\n return { nodes, ...(typeof rootId === 'string' ? { rootId } : {}) }\n}\n\n/** Every other page's search title, for a proposal not to repeat. */\nasync function siteTitles(hostRef: DocumentReference, except: ReadonlySet<string>): Promise<string[]> {\n const snapshot = await hostRef.collection('screens').select('seo', 'deletedAt').limit(1000).get()\n return snapshot.docs\n .filter((doc) => !except.has(doc.id) && !doc.get('deletedAt'))\n .map((doc) => str((doc.get('seo') as SeoScreenDocument['seo'])?.title))\n .filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * One listing: a page or a product\n * ------------------------------------------------------------------------ */\n\nfunction listingOutcome(\n generation: AiValidatedGeneration<AiSeoFieldValues>,\n job: AiJob,\n subject: AiSeoFieldsProposal['subject'],\n context: {\n keywords: readonly string[]\n facts: AiSeoPageFacts | null\n fields: readonly SeoListingFieldKey[]\n hasImage: boolean\n },\n): AiJobStepOutcome {\n const spent = spentBy(generation)\n if (generation.status === 'refused') return { outputs: [], ...spent, refused: true }\n if (generation.status === 'needs_input') return { outputs: [], ...spent, failure: generation.message }\n const values = generation.value\n const notes: string[] = []\n if (context.fields.includes('imageAlt') && context.hasImage && !values.imageAlt) {\n notes.push('No image description was proposed: nothing on the page says what the picture shows.')\n }\n const proposal: AiSeoFieldsProposal = {\n kind: 'fields',\n subject,\n values,\n keywords: aiSeoKeywordCoverage(context.keywords, {\n title: values.title,\n description: values.description,\n h1: context.facts?.h1s[0]?.text,\n body: context.facts?.text,\n }),\n notes,\n }\n const output: AiJobOutput = {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.fields(subject.kind, subject.id),\n hostId: job.hostId ?? null,\n label: `SEO proposal · ${subject.name}`,\n proposal: plain(proposal),\n }\n return { outputs: [output], ...spent }\n}\n\nasync function runScreenListing(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const screenId = str(job.inputs?.['screenId'])\n const snapshot = screenId ? await hostRef.collection('screens').doc(screenId).get() : null\n const screen = snapshot?.exists ? (snapshot.data() as SeoScreenDocument) : null\n if (!screen || screen.deletedAt) {\n return { outputs: [], ...spentNothing(model), failure: AI_SEO_NO_PAGE_COPY }\n }\n\n const versionId = str(job.inputs?.['versionId']) || str(screen.versionId)\n const version = await readVersionNodes(hostRef, screenId, versionId)\n const facts = aiSeoPageFacts(version?.nodes as never, { rootId: version?.rootId })\n const fields = aiSeoRequestedFields(job.inputs?.['fields'], SCREEN_SEO_LISTING_FIELDS)\n const seo = screen.seo ?? {}\n const hasImage = Boolean(str(seo.image))\n // The share image is often a picture the page shows too: its description\n // and the text beside it there are what is known about it.\n const onPage = hasImage ? facts.images.find((image) => image.src === str(seo.image)) : undefined\n const route = host.screens?.[screenId]\n const path = typeof route === 'string' ? screenRoutePathToUrl(route) : null\n const name = str(screen.displayName) || path || 'Untitled page'\n const keywords = aiSeoKeywordList(job.inputs?.['keywords'])\n const generation = await generateSeoFields({\n subject: { kind: 'screen', name, path },\n brand: brandOf(host),\n text: facts.text || str(screen.description),\n fields,\n current: {\n title: str(seo.title),\n description: str(seo.description),\n breadcrumb: str(seo.breadcrumb),\n imageAlt: str(seo.imageAlt),\n },\n image: hasImage ? { alt: str(seo.imageAlt) || onPage?.alt || null, context: onPage?.context || null } : null,\n keywords,\n otherTitles: await siteTitles(hostRef, new Set([screenId])),\n model,\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIELDS_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n })\n return listingOutcome(generation, job, { kind: 'screen', id: screenId, name, path }, {\n keywords,\n facts,\n fields,\n hasImage,\n })\n}\n\nasync function runProductListing(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, signal } = context\n const model = modelOf(context)\n const name = str(job.inputs?.['name']).slice(0, 200)\n if (!name) return { outputs: [], ...spentNothing(model), failure: AI_SEO_NO_PRODUCT_COPY }\n const fields = aiSeoRequestedFields(job.inputs?.['fields'], PRODUCT_LISTING_FIELDS)\n const keywords = aiSeoKeywordList(job.inputs?.['keywords'])\n const productId = str(job.inputs?.['productId']) || null\n const generation = await generateSeoFields({\n subject: { kind: 'product', name },\n brand: brandOf(host),\n // The product's own words, as its editor handed them over: the product\n // document is the commerce plugin's, and this step never reads it.\n text: str(job.inputs?.['text']).slice(0, AI_SEO_PAGE_TEXT_MAX_CHARS),\n fields,\n current: {\n title: str(job.inputs?.['currentTitle']),\n description: str(job.inputs?.['currentDescription']),\n },\n image: null,\n keywords,\n model,\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIELDS_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n })\n return listingOutcome(generation, job, { kind: 'product', id: productId, name }, {\n keywords,\n facts: null,\n fields,\n hasImage: false,\n })\n}\n\n/* ------------------------------------------------------------------------ *\n * The site audit\n * ------------------------------------------------------------------------ */\n\nexport interface AiSeoScannedPage extends AiSeoAuditPage {\n /** The page's node map as audited. */\n nodes: Record<string, unknown> | null\n}\n\nexport interface AiSeoSiteScan {\n pages: AiSeoScannedPage[]\n skipped: number\n notes: string[]\n}\n\n/**\n * Read the site the way its sitemap lists it: every screen the routing map\n * publishes, less the template screens, the status pages and every screen\n * that is not public — through the same shared predicates the tenant's\n * `sitemap.xml` and `/llms.txt` apply, so the audit covers exactly the pages\n * a crawler is handed. Then each page's published version, and the shared\n * layouts, for the links that decide which pages nothing points to.\n */\nexport async function scanAiSeoSite(\n firestore: Firestore,\n hostId: string,\n host: SeoHostDocument,\n rawKeywords: unknown,\n): Promise<AiSeoSiteScan> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const routing = host.screens ?? {}\n const [screenDocs, templateScreenIds, layoutDocs] = await Promise.all([\n hostRef\n .collection('screens')\n .select('visibility', 'seo', 'description', 'displayName', 'versionId', 'deletedAt')\n .limit(1000)\n .get(),\n getTemplateScreenIds({ hostId }),\n hostRef.collection('layouts').select('versionId').limit(AI_SEO_LAYOUT_SCAN_LIMIT).get(),\n ])\n const screens = new Map(screenDocs.docs.map((doc) => [doc.id, doc.data() as SeoScreenDocument]))\n const excluded = new Set<string>([...statusPageScreenIds(host as never), ...templateScreenIds])\n const published = Object.entries(routing)\n .filter(([screenId]) => !excluded.has(screenId))\n .filter(([screenId]) => {\n const screen = screens.get(screenId)\n return Boolean(screen) && !screen?.deletedAt && isScreenIndexable(screen as never)\n })\n .map(([screenId, route]) => ({ screenId, path: screenRoutePathToUrl(String(route)) }))\n .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))\n const audited = published.slice(0, AI_SEO_AUDIT_MAX_PAGES)\n\n const versions = new Map<string, { nodes: Record<string, unknown>; rootId?: string } | null>()\n for (let start = 0; start < audited.length; start += READ_CHUNK) {\n await Promise.all(\n audited.slice(start, start + READ_CHUNK).map(async ({ screenId }) => {\n versions.set(\n screenId,\n await readVersionNodes(hostRef, screenId, str(screens.get(screenId)?.versionId)),\n )\n }),\n )\n }\n const layoutNodes: Record<string, unknown>[] = []\n const layouts = layoutDocs.docs.filter((doc) => str(doc.get('versionId')))\n for (let start = 0; start < layouts.length; start += READ_CHUNK) {\n await Promise.all(\n layouts.slice(start, start + READ_CHUNK).map(async (doc) => {\n const snapshot = await doc.ref.collection('versions').doc(str(doc.get('versionId'))).get()\n const nodes = snapshot.exists\n ? decodeStoredNodes<Record<string, unknown>>(snapshot.get('nodes'))\n : null\n if (nodes) layoutNodes.push(nodes)\n }),\n )\n }\n\n const keywordsByPath = parseAiSeoKeywordLines(rawKeywords)\n const auditedPaths = new Set(audited.map((page) => aiSeoNormalizePath(page.path)))\n const notes = Object.keys(keywordsByPath)\n .filter((path) => !auditedPaths.has(path))\n .map((path) => `Keywords for ${path} were not used: no audited page is published at that address.`)\n\n const pages: AiSeoScannedPage[] = audited.map(({ screenId, path }) => {\n const screen = screens.get(screenId) ?? {}\n const version = versions.get(screenId) ?? null\n const linkedFrom =\n layoutNodes.some((nodes) => nodesReferenceScreen(nodes, screenId)) ||\n audited.some(\n (other) =>\n other.screenId !== screenId &&\n nodesReferenceScreen(versions.get(other.screenId)?.nodes ?? null, screenId),\n )\n return {\n screenId,\n path,\n name: str(screen.displayName) || path,\n versionId: str(screen.versionId) || null,\n seo: screen.seo ?? {},\n description: str(screen.description),\n facts: aiSeoPageFacts(version?.nodes as never, { rootId: version?.rootId }),\n linkedFrom,\n keywords: keywordsByPath[aiSeoNormalizePath(path)] ?? [],\n nodes: version?.nodes ?? null,\n }\n })\n return { pages, skipped: Math.max(0, published.length - audited.length), notes }\n}\n\n/** One unit of generated work an audit still owes. */\nexport type AiSeoAuditUnit =\n | { kind: 'site' }\n | { kind: 'batch'; batch: number; of: number; screenIds: string[] }\n\n/** The units an audit owes after the outputs recorded so far, in the order they run. */\nexport function aiSeoPendingUnits(\n report: AiSeoAuditReport,\n outputs: readonly Pick<AiJobOutput, 'id'>[],\n): AiSeoAuditUnit[] {\n const done = new Set(outputs.map((output) => output.id))\n const units: AiSeoAuditUnit[] = []\n if (report.siteProposal && !done.has(AI_SEO_OUTPUT_IDS.site)) units.push({ kind: 'site' })\n const batches = aiSeoAuditBatches(report)\n batches.forEach((screenIds, index) => {\n if (!done.has(AI_SEO_OUTPUT_IDS.fixes(index + 1))) {\n units.push({ kind: 'batch', batch: index + 1, of: batches.length, screenIds })\n }\n })\n return units\n}\n\ninterface UnitResult {\n output: AiJobOutput\n spent: Spent\n}\n\n/* ---- The site-wide proposal ---------------------------------------------- */\n\nexport const AI_SEO_SITE_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose two things for a website: the structured data that says who publishes it, and ' +\n 'the guidance an AI agent reads first in its /llms.txt.\\n\\n' +\n `Answer by calling ${AI_SEO_SITE_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n '- Fill only the fields you are told are empty; answer null for every other one.\\n' +\n '- Use only what the pages you are given state. A contact email or telephone number goes in ' +\n 'only when a page shows it exactly; never compose one.\\n' +\n '- The entity description says what the publisher is, in one plain sentence, not what one page ' +\n 'offers.\\n' +\n '- The agent guidance is checkable and specific: the questions this site answers and which ' +\n 'pages answer them. No marketing language: an agent discounts a claim it cannot check.\\n' +\n '- Keep every length the tool states, and write in the language of the pages.',\n cacheBreakpoint: true,\n },\n]\n\n/** The site SEO form fields blank on the site now. */\nexport function aiSeoBlankSiteFields(host: SeoHostDocument): Set<AiSeoSiteFormField> {\n const entity = host.seo?.entity ?? {}\n const agent = host.seo?.agent ?? {}\n const blank = new Set<AiSeoSiteFormField>()\n if (entity.type === undefined || entity.type === null || entity.type === '') blank.add('seo.entity.type')\n if (!str(entity.name)) blank.add('seo.entity.name')\n if (!str(entity.description)) blank.add('seo.entity.description')\n if (!str(entity.email)) blank.add('seo.entity.email')\n if (!str(entity.telephone)) blank.add('seo.entity.telephone')\n if (!str(entity.contactType)) blank.add('seo.entity.contactType')\n if (!str(agent.whenToUse)) blank.add('seo.agent.whenToUse')\n if (!str(agent.howToUse)) blank.add('seo.agent.howToUse')\n return blank\n}\n\n/** The site's content collections, for the llms.txt preview. */\nasync function readContentCollections(hostRef: DocumentReference): Promise<LlmsTxtCollection[]> {\n try {\n const snapshot = await hostRef.collection('collections').select('slug', 'displayName', 'kind').limit(50).get()\n return snapshot.docs\n .filter((doc) => hostCollectionKind(doc.data()) === 'content' && str(doc.get('slug')))\n .map((doc) => ({ slug: str(doc.get('slug')), name: str(doc.get('displayName')) || undefined }))\n } catch {\n // A preview without the feeds is still a preview of the guidance.\n return []\n }\n}\n\n/** `/llms.txt` as it reads with `values` saved over what the site stores now. */\nexport function aiSeoLlmsPreview(\n host: SeoHostDocument,\n values: Partial<Record<AiSeoSiteFormField, string>>,\n pages: readonly LlmsTxtPage[],\n collections: readonly LlmsTxtCollection[],\n): string {\n return buildLlmsTxt({\n siteName: str(host.seo?.title) || str(host.displayName) || 'Site',\n origin: hostPublicOrigin(host) ?? '',\n description: str(host.seo?.description) || undefined,\n agent: {\n whenToUse: values['seo.agent.whenToUse'] ?? host.seo?.agent?.whenToUse,\n howToUse: values['seo.agent.howToUse'] ?? host.seo?.agent?.howToUse,\n },\n pages,\n collections,\n hasSearch: true,\n contactEmail: values['seo.entity.email'] ?? (str(host.seo?.entity?.email) || undefined),\n })\n}\n\nasync function proposeSite(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n scan: AiSeoSiteScan,\n): Promise<UnitResult> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const blank = aiSeoBlankSiteFields(host)\n const collections = await readContentCollections(hostRef)\n const topLevel: LlmsTxtPage[] = scan.pages\n .filter((page) => !page.path.replace(/^\\//, '').includes('/'))\n .slice(0, SITE_PAGES_LISTED)\n .map((page) => ({ path: page.path, ...(page.name ? { title: page.name } : {}) }))\n // The pages that say who publishes a site: the home page, and whatever is\n // about the company or how to reach it.\n const telling = scan.pages\n .filter((page) => page.path === '/' || /about|contact|company|team|who/i.test(page.path))\n .slice(0, 4)\n const prompt = [\n `Site: ${brandOf(host) || 'untitled site'}`,\n host.seo?.description ? `Site description: ${str(host.seo.description)}` : '',\n `Empty fields you may fill: ${[...blank].join(', ') || 'none'}`,\n 'Pages:',\n ...scan.pages\n .slice(0, SITE_PAGES_LISTED)\n .map((page) => `- ${page.path} — ${page.name}${str(page.seo.title) ? ` — ${str(page.seo.title)}` : ''}`),\n collections.length\n ? `Content collections: ${collections.map((entry) => entry.name || entry.slug).join(', ')}`\n : '',\n ...telling.flatMap((page) => ['', `Text of ${page.path}:`, page.facts.text.slice(0, 1_200) || '(no text)']),\n ]\n .filter((line) => line !== '')\n .join('\\n')\n const siteText = scan.pages.map((page) => page.facts.text).join('\\n')\n const generation = await runValidatedGeneration('seo-site', {\n step: 'job.seo',\n model,\n instructions: AI_SEO_SITE_INSTRUCTIONS,\n messages: [{ role: 'user', content: prompt }],\n tool: aiSeoSiteTool(),\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_SITE_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n check: (answer) => checkAiSeoSite(answer, { blank, siteText }),\n })\n const proposed = generation.status === 'ok' ? generation.value : { values: {}, notes: [] }\n const notes =\n generation.status === 'ok'\n ? proposed.notes\n : [\n generation.status === 'refused'\n ? 'The AI declined to propose structured data for this site.'\n : generation.message,\n ]\n const site: AiSeoSiteProposal = {\n values: proposed.values,\n llmsPreview: aiSeoLlmsPreview(host, proposed.values, topLevel, collections),\n notes,\n }\n return {\n output: {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.site,\n hostId: job.hostId ?? null,\n label: 'SEO audit · structured data and agent guidance',\n proposal: plain({ kind: 'site', site }),\n },\n spent: spentBy(generation),\n }\n}\n\n/* ---- A batch of page fixes ------------------------------------------------ */\n\nexport const AI_SEO_FIXES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You fix the search listings and headings of a website’s pages. For each page you are given ' +\n 'what an audit found, what the page shows now, and its text.\\n\\n' +\n `Answer by calling ${AI_SEO_FIXES_TOOL_NAME} exactly once, with one entry per page. A reply in ` +\n 'prose cannot be used.\\n\\n' +\n 'Rules:\\n' +\n '- Fix only what the audit found. Answer null for a value it did not find a problem with.\\n' +\n '- Write only from the page’s own text. Never invent a fact, a price, a name, a place or a claim.\\n' +\n '- A title says what the page is, specifically. No two pages share a title, and none repeats a ' +\n 'title you are told another page uses.\\n' +\n '- A description is one or two sentences telling a searcher what they will find.\\n' +\n '- The main heading names what the page is about in a few plain words.\\n' +\n '- An image description says what the picture most likely shows, from the text beside it. ' +\n 'Leave an image out when the text gives no idea.\\n' +\n '- A target keyword goes in only where the page is about it and it reads naturally: at most ' +\n 'once in a title and once in a description. Never list keywords or repeat a word to rank.\\n' +\n '- Keep every length the tool states, and write in the language of each page.',\n cacheBreakpoint: true,\n },\n]\n\n/** The fixes one page gets: the model's values, its content fixes, and the guidance no fix can give. */\nexport function aiSeoPageFixFor(page: AiSeoBatchPage, answer: AiSeoBatchAnswer | null): AiSeoPageFix {\n const values: AiSeoFieldValues = {}\n if (answer?.title) values.title = answer.title\n if (answer?.description) values.description = answer.description\n const content: AiSeoContentFix[] = []\n for (const alt of answer?.imageAlts ?? []) content.push({ kind: 'image-alt', nodeId: alt.nodeId, alt: alt.alt })\n if (answer?.h1) {\n const first = page.facts.h1s[0]\n const promotable = page.facts.headings.find((heading) => heading.editable && heading.nodeId)\n if (page.codes.has('h1-thin') && first?.editable && first.nodeId) {\n content.push({ kind: 'h1-set', nodeId: first.nodeId, text: answer.h1 })\n } else if (page.codes.has('h1-missing') && promotable?.nodeId) {\n content.push({ kind: 'h1-set', nodeId: promotable.nodeId, text: answer.h1 })\n } else if (page.codes.has('h1-missing')) {\n content.push({ kind: 'h1-insert', text: answer.h1 })\n }\n }\n if (page.codes.has('h1-multiple')) content.push(...aiSeoHeadingDemotions(page.facts))\n const guidance: string[] = []\n if (page.codes.has('orphan')) {\n guidance.push('Link to this page from your navigation or a related page. Where it belongs is yours to choose.')\n }\n return { screenId: page.screenId, values, content, guidance }\n}\n\n/** The user turn for one batch: each page's findings, what it shows now, and its text. */\nexport function aiSeoFixesPrompt(pages: readonly AiSeoBatchPage[], titlesElsewhere: readonly string[]): string {\n const titles = titlesElsewhere.length\n ? `Titles other pages use:\\n${titlesElsewhere.slice(0, 40).map((title) => `- ${title}`).join('\\n')}\\n\\n`\n : ''\n const blocks = pages.map((page) => {\n const lines = [\n `### Page ${page.screenId}: ${page.name} (${page.path})`,\n `Found: ${[...page.codes].map((code) => AI_SEO_FINDING_LABELS[code]).join('; ')}`,\n `Title now: ${str(page.seo?.title) || '(none)'}`,\n `Description now: ${str(page.seo?.description) || '(none)'}`,\n `Main heading now: ${page.facts.h1s[0]?.text || '(none)'}`,\n ]\n if (page.keywords.length) lines.push(`Target keywords: ${page.keywords.join(', ')}`)\n const images = aiSeoFixImages(page)\n if (images.length) {\n lines.push(\n 'Images without a description:',\n ...images.map((image) => `- ${image.nodeId}: beside \"${image.context || 'no text'}\"`),\n )\n }\n lines.push('Text:', page.facts.text.slice(0, AI_SEO_BATCH_PAGE_TEXT_CHARS) || '(no text)')\n return lines.join('\\n')\n })\n return `${titles}${blocks.join('\\n\\n')}`\n}\n\nasync function proposeFixes(\n context: AiJobStepContext & { firestore: Firestore },\n report: AiSeoAuditReport,\n unit: Extract<AiSeoAuditUnit, { kind: 'batch' }>,\n): Promise<UnitResult> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const reports = new Map(report.pages.map((page) => [page.screenId, page]))\n // Read now, not as audited: a page edited since keeps its edits, and one\n // deleted since is skipped.\n const loaded = await Promise.all(\n unit.screenIds.map(async (screenId): Promise<AiSeoBatchPage | null> => {\n const pageReport = reports.get(screenId)\n const snapshot = await hostRef.collection('screens').doc(screenId).get()\n const screen = snapshot.exists ? (snapshot.data() as SeoScreenDocument) : null\n if (!pageReport || !screen || screen.deletedAt) return null\n const version = await readVersionNodes(hostRef, screenId, str(screen.versionId))\n return {\n screenId,\n path: pageReport.path,\n name: pageReport.name,\n codes: new Set(pageReport.findings.map((entry) => entry.code)),\n keywords: pageReport.keywords.map((entry) => entry.keyword),\n seo: screen.seo ?? {},\n facts: aiSeoPageFacts(version?.nodes as never, { rootId: version?.rootId }),\n }\n }),\n )\n const pages = loaded.filter((page): page is AiSeoBatchPage => page !== null)\n const titlesElsewhere = await siteTitles(hostRef, new Set(pages.map((page) => page.screenId)))\n\n let spent = spentNothing(model)\n let answers: Record<string, AiSeoBatchAnswer> = {}\n const notes: string[] = []\n if (pages.length) {\n const generation = await runValidatedGeneration('seo-fixes', {\n step: 'job.seo',\n model,\n instructions: AI_SEO_FIXES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiSeoFixesPrompt(pages, titlesElsewhere) }],\n tool: aiSeoFixesTool(pages.map((page) => page.screenId)),\n // Lowered on a tier too slow to answer a whole batch and ask again\n // inside the least time the step registers.\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIXES_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n check: (answer) => checkAiSeoFixes(answer, pages, titlesElsewhere),\n })\n spent = spentBy(generation)\n if (generation.status === 'ok') {\n answers = generation.value\n } else {\n notes.push(\n generation.status === 'refused'\n ? 'The AI declined to propose fixes for these pages.'\n : generation.message,\n )\n }\n }\n const fixes = pages.map((page) => aiSeoPageFixFor(page, answers[page.screenId] ?? null))\n const skipped = unit.screenIds.length - pages.length\n if (skipped > 0) {\n notes.push(`${skipped} ${skipped === 1 ? 'page was' : 'pages were'} deleted since the audit and left out.`)\n }\n return {\n output: {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.fixes(unit.batch),\n hostId: job.hostId ?? null,\n label: `SEO audit · fixes ${unit.batch} of ${unit.of}`,\n proposal: plain({ kind: 'fixes', batch: unit.batch, fixes, notes }),\n },\n spent,\n }\n}\n\nasync function runSiteAuditPass(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, firestore } = context\n const model = modelOf(context)\n const recorded = aiSeoAuditView(job.outputs ?? [])\n const outputs: AiJobOutput[] = []\n let spent = spentNothing(model)\n let report: AiSeoAuditReport\n let scan: AiSeoSiteScan | null = null\n if (recorded) {\n report = recorded.report\n } else {\n scan = await scanAiSeoSite(firestore, job.hostId as string, host, job.inputs?.['keywords'])\n report = aiSeoAudit(\n scan.pages,\n {\n discouraged: isSearchDiscouraged(host as never),\n entity: host.seo?.entity ?? {},\n agent: host.seo?.agent ?? {},\n },\n { skipped: scan.skipped },\n )\n report.notes.push(...scan.notes)\n const findings = report.pages.reduce((sum, page) => sum + page.findings.length, report.site.length)\n outputs.push({\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.report,\n hostId: job.hostId ?? null,\n label: `SEO audit · ${report.pages.length} ${report.pages.length === 1 ? 'page' : 'pages'} · ${findings} ${findings === 1 ? 'finding' : 'findings'}`,\n proposal: plain(report),\n })\n }\n const units = aiSeoPendingUnits(report, [...(job.outputs ?? []), ...outputs])\n if (!units.length) return { outputs, ...spent }\n const [unit] = units\n const result =\n unit.kind === 'site'\n ? await proposeSite(\n context,\n host,\n scan ?? (await scanAiSeoSite(firestore, job.hostId as string, host, job.inputs?.['keywords'])),\n )\n : await proposeFixes(context, report, unit)\n outputs.push(result.output)\n spent = addSpent(spent, result.spent)\n return { outputs, ...spent, ...(units.length > 1 ? { continue: true } : {}) }\n}\n\nexport const runAiJobSeoStep: AiJobStepRunner = async (context) => {\n const { job, firestore } = context\n if (!firestore) {\n // The machine always hands one in; a runner called without it is a\n // wiring fault, not a customer path.\n throw new Error('the SEO step reads the site it writes about and was given no Firestore')\n }\n const target = aiSeoJobTarget(job.inputs)\n if (!target) return { outputs: [], ...spentNothing(modelOf(context)), failure: AI_SEO_NO_TARGET_COPY }\n const site = await loadSite(firestore, job)\n if ('failure' in site) return { outputs: [], ...spentNothing(modelOf(context)), failure: site.failure }\n const withFirestore = { ...context, firestore }\n switch (target) {\n case 'screen':\n return runScreenListing(withFirestore, site.host)\n case 'product':\n return runProductListing(withFirestore, site.host)\n case 'site':\n return runSiteAuditPass(withFirestore, site.host)\n }\n}\n"],"names":["hostCollectionKind","hostPublicOrigin","buildLlmsTxt","nodesReferenceScreen","screenRoutePathToUrl","isScreenIndexable","isSearchDiscouraged","statusPageScreenIds","SCREEN_SEO_LISTING_FIELDS","isSeoListingFieldKey","decodeStoredNodes","getTemplateScreenIds","AI_SEO_FINDING_LABELS","AI_SEO_OUTPUT_IDS","aiSeoAuditView","aiSeoJobTarget","aiModelForStep","AI_SEO_AUDIT_MAX_PAGES","aiSeoAudit","aiSeoAuditBatches","aiSeoHeadingDemotions","aiSeoNormalizePath","parseAiSeoKeywordLines","runValidatedGeneration","AI_SEO_FIELDS_MAX_TOKENS","generateSeoFields","AI_SEO_PAGE_TEXT_MAX_CHARS","aiSeoPageFacts","AI_SEO_FIXES_TOOL_NAME","AI_SEO_SITE_TOOL_NAME","aiSeoFixImages","aiSeoFixesTool","aiSeoKeywordCoverage","aiSeoKeywordList","aiSeoSiteTool","checkAiSeoFixes","checkAiSeoSite","AI_SEO_FIXES_MAX_TOKENS","AI_SEO_SITE_MAX_TOKENS","aiSeoGenerationMaxTokens","AI_SEO_NO_SITE_COPY","AI_SEO_NO_PAGE_COPY","AI_SEO_NO_TARGET_COPY","AI_SEO_NO_PRODUCT_COPY","AI_SEO_LAYOUT_SCAN_LIMIT","AI_SEO_BATCH_PAGE_TEXT_CHARS","READ_CHUNK","SITE_PAGES_LISTED","PRODUCT_LISTING_FIELDS","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","round6","Math","round","modelOf","context","modelFor","spentNothing","model","usage","estCostUsd","stopReason","spentBy","generation","addSpent","a","b","plain","JSON","parse","stringify","aiSeoRequestedFields","raw","allowed","named","String","split","map","part","filter","wanted","Set","length","key","has","brandOf","host","seo","title","displayName","subdomain","loadSite","firestore","job","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","readVersionNodes","hostRef","screenId","versionId","nodes","rootId","siteTitles","except","select","limit","docs","id","Boolean","listingOutcome","subject","spent","status","outputs","refused","message","values","notes","fields","includes","hasImage","imageAlt","push","proposal","kind","keywords","description","h1","facts","h1s","text","body","output","resource","label","name","runScreenListing","screen","signal","inputs","deletedAt","version","image","onPage","images","find","src","undefined","route","screens","path","brand","current","breadcrumb","alt","otherTitles","maxTokens","runProductListing","slice","productId","scanAiSeoSite","rawKeywords","routing","screenDocs","templateScreenIds","layoutDocs","Promise","all","Map","excluded","published","Object","entries","sort","audited","versions","start","set","layoutNodes","layouts","ref","keywordsByPath","auditedPaths","page","keys","pages","linkedFrom","some","other","skipped","max","aiSeoPendingUnits","report","done","units","siteProposal","site","batches","forEach","screenIds","index","fixes","batch","of","AI_SEO_SITE_INSTRUCTIONS","cacheBreakpoint","aiSeoBlankSiteFields","entity","agent","blank","type","add","email","telephone","contactType","whenToUse","howToUse","readContentCollections","slug","aiSeoLlmsPreview","collections","siteName","origin","hasSearch","contactEmail","proposeSite","scan","topLevel","replace","telling","test","prompt","join","entry","flatMap","line","siteText","step","instructions","messages","role","content","tool","check","answer","proposed","llmsPreview","AI_SEO_FIXES_INSTRUCTIONS","aiSeoPageFixFor","imageAlts","nodeId","first","promotable","headings","heading","editable","codes","guidance","aiSeoFixesPrompt","titlesElsewhere","titles","blocks","lines","code","proposeFixes","unit","reports","loaded","pageReport","findings","keyword","answers","runSiteAuditPass","recorded","discouraged","reduce","sum","result","continue","runAiJobSeoStep","Error","target","withFirestore"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-seo-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n buildLlmsTxt,\n type LlmsTxtCollection,\n type LlmsTxtPage,\n} from '@aglyn/aglyn/app-utils/llms-txt'\nimport { screenRoutePathToUrl } from '@aglyn/aglyn/app-utils/screen-route'\nimport { SEO_FINDING_LABELS } from '@aglyn/aglyn/app-utils/seo-audit'\nimport { seoKeywordCoverage, seoKeywordList } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n SCREEN_SEO_LISTING_FIELDS,\n isSeoListingFieldKey,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport {\n SEO_PAGE_TEXT_MAX_CHARS,\n seoPageFacts,\n type SeoPageFacts,\n} from '@aglyn/aglyn/app-utils/seo-page-facts'\nimport { scanSeoSite, seoAuditSiteOf, type SeoSiteScan } from '@aglyn/aglyn/app-utils/seo-site-scan'\nimport { decodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { getTemplateScreenIds } from '@aglyn/tenant-runtime/template-screens'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_SEO_OUTPUT_IDS,\n aiSeoAuditView,\n aiSeoJobTarget,\n type AiSeoAuditReport,\n type AiSeoContentFix,\n type AiSeoFieldsProposal,\n type AiSeoFieldValues,\n type AiSeoPageFix,\n type AiSeoSiteFormField,\n type AiSeoSiteProposal,\n} from '../model/ai-seo'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiSystemBlock, AiUsage } from '../runtime/ai-runtime'\nimport { aiSeoAudit, aiSeoAuditBatches, aiSeoHeadingDemotions } from '../runtime/seo-audit'\nimport {\n runValidatedGeneration,\n type AiGenerationSpend,\n type AiValidatedGeneration,\n} from '../runtime/ai-doctrine'\nimport { AI_SEO_FIELDS_MAX_TOKENS, generateSeoFields } from '../runtime/seo-fields'\nimport {\n AI_SEO_FIXES_TOOL_NAME,\n AI_SEO_SITE_TOOL_NAME,\n aiSeoFixImages,\n aiSeoFixesTool,\n aiSeoSiteTool,\n checkAiSeoFixes,\n checkAiSeoSite,\n type AiSeoBatchAnswer,\n type AiSeoBatchPage,\n} from '../tools/ai-seo-tool'\nimport {\n AI_SEO_FIXES_MAX_TOKENS,\n AI_SEO_SITE_MAX_TOKENS,\n aiSeoGenerationMaxTokens,\n} from './ai-job-seo-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The `seo` step (AGL-2910): search listings and a site audit, as proposals.\n *\n * `inputs.target` says what the job is about:\n *\n * - `screen` — one page's listing, written from the version the person is\n * looking at (`screenId`, `versionId`), for the page's SEO card;\n * - `product` — one product's listing, from the name and description the\n * product editor handed over (`name`, `text`), for the product editor;\n * - `site` — the audit of every published page the sitemap lists, with a\n * proposed fix per finding, structured data for the site's entity, and the\n * agent guidance `/llms.txt` leads with.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. A listing is served\n * straight from the screen document, the structured data and `/llms.txt`\n * from the host document, and a page's content from its published version,\n * so the values ride on the job's outputs and nothing else: a person puts\n * them in the page's SEO card or the site SEO form and saves them there, and\n * content fixes become a new version only through the apply door.\n *\n * ## An audit, a unit at a time\n *\n * The first pass reads the site — every published page's version and the\n * shared layouts, for the links between them — and scores it without a\n * model, through the platform's SEO check (the same findings the site's SEO\n * section lists to every owner); it records the report, and runs the first unit of generated work. Each later\n * pass runs one more unit: the site-wide proposal, then the page fixes in\n * batches. Every pass is one reservation and one provider exchange, and it\n * asks the machine to continue until nothing is left, so a large site is a\n * few beats of work rather than one request that cannot finish.\n */\n\nexport const AI_SEO_NO_SITE_COPY = 'This SEO job names a site this workspace does not have.'\nexport const AI_SEO_NO_PAGE_COPY = 'This page is no longer on the site.'\nexport const AI_SEO_NO_TARGET_COPY = 'This SEO job does not say what to write about.'\nexport const AI_SEO_NO_PRODUCT_COPY = 'Give the product a name before writing its listing.'\n\n/** How much of each page a batch of fixes carries. */\nexport const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700\n\n/**\n * Output budgets, declared with the step's time (`ai-job-seo-budget.ts`,\n * AGL-3035). A batch's largest answer — every page with a title, description,\n * heading and image descriptions at their limits — and the site proposal's are\n * measured in `ai-job-seo-step.spec.ts`.\n */\nexport { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS }\n\n/** Pages the site prompt and the llms.txt preview list. */\nconst SITE_PAGES_LISTED = 25\n\n/** The fields a product's listing holds. */\nconst PRODUCT_LISTING_FIELDS: readonly SeoListingFieldKey[] = ['title', 'description']\n\nconst ZERO_USAGE: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\ntype DocumentReference = FirebaseFirestore.DocumentReference\ntype Spent = Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n\n/** The host document fields this step reads. */\nexport interface SeoHostDocument {\n orgId?: string\n displayName?: string\n subdomain?: string | null\n cname?: string | null\n screens?: Record<string, string>\n errorScreens?: Record<string, string>\n notFoundScreenId?: string | null\n seo?: {\n title?: string\n description?: string\n discourageSearchEngines?: boolean\n entity?: {\n type?: unknown\n name?: string\n description?: string\n email?: string\n telephone?: string\n contactType?: string\n }\n agent?: { whenToUse?: string; howToUse?: string }\n }\n}\n\n/** The screen document fields this step reads. */\ninterface SeoScreenDocument {\n displayName?: string\n description?: string\n versionId?: string\n visibility?: unknown\n deletedAt?: unknown\n seo?: {\n title?: string\n description?: string\n breadcrumb?: string\n image?: string\n imageAlt?: string\n }\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst round6 = (value: number): number => Math.round(value * 1_000_000) / 1_000_000\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.('job.seo') ?? aiModelForStep('job.seo')\n}\n\nfunction spentNothing(model: string): Spent {\n return { usage: ZERO_USAGE, estCostUsd: 0, model, stopReason: null }\n}\n\nfunction spentBy(generation: AiGenerationSpend): Spent {\n return {\n usage: generation.usage,\n estCostUsd: generation.estCostUsd,\n model: generation.model,\n stopReason: generation.stopReason,\n }\n}\n\nfunction addSpent(a: Spent, b: Spent): Spent {\n return {\n usage: {\n inputTokens: a.usage.inputTokens + b.usage.inputTokens,\n outputTokens: a.usage.outputTokens + b.usage.outputTokens,\n cacheReadTokens: a.usage.cacheReadTokens + b.usage.cacheReadTokens,\n cacheWriteTokens: a.usage.cacheWriteTokens + b.usage.cacheWriteTokens,\n },\n estCostUsd: round6(a.estCostUsd + b.estCostUsd),\n model: b.model || a.model,\n stopReason: b.stopReason ?? a.stopReason,\n }\n}\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/**\n * The fields a listing job writes: the ones the editor named (`inputs.fields`,\n * comma-separated) that the target's editor holds, or all of those.\n */\nexport function aiSeoRequestedFields(\n raw: unknown,\n allowed: readonly SeoListingFieldKey[],\n): SeoListingFieldKey[] {\n const named = String(raw ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter(isSeoListingFieldKey)\n const wanted = new Set<SeoListingFieldKey>(named.length ? named : allowed)\n return allowed.filter((key) => wanted.has(key))\n}\n\n/** The site's name, as its visitors read it. */\nfunction brandOf(host: SeoHostDocument): string {\n return str(host.seo?.title) || str(host.displayName) || str(host.subdomain)\n}\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadSite(\n firestore: Firestore,\n job: AiJob,\n): Promise<{ host: SeoHostDocument } | { failure: string }> {\n if (!job.hostId) return { failure: AI_SEO_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as SeoHostDocument) : null\n // The route checks the caller's membership of the org the job is metered\n // against, not that the site belongs to it. A site of another org is\n // refused here, before anything about it reaches a prompt.\n if (!host || host.orgId !== job.orgId) return { failure: AI_SEO_NO_SITE_COPY }\n return { host }\n}\n\n/** A version's node map and root, or `null` when it cannot be read. */\nasync function readVersionNodes(\n hostRef: DocumentReference,\n screenId: string,\n versionId: string,\n): Promise<{ nodes: Record<string, unknown>; rootId?: string } | null> {\n if (!versionId) return null\n const snapshot = await hostRef\n .collection('screens')\n .doc(screenId)\n .collection('versions')\n .doc(versionId)\n .get()\n if (!snapshot.exists) return null\n const nodes = decodeStoredNodes<Record<string, unknown>>(snapshot.get('nodes'))\n if (!nodes) return null\n const rootId = snapshot.get('rootId')\n return { nodes, ...(typeof rootId === 'string' ? { rootId } : {}) }\n}\n\n/** Every other page's search title, for a proposal not to repeat. */\nasync function siteTitles(hostRef: DocumentReference, except: ReadonlySet<string>): Promise<string[]> {\n const snapshot = await hostRef.collection('screens').select('seo', 'deletedAt').limit(1000).get()\n return snapshot.docs\n .filter((doc) => !except.has(doc.id) && !doc.get('deletedAt'))\n .map((doc) => str((doc.get('seo') as SeoScreenDocument['seo'])?.title))\n .filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * One listing: a page or a product\n * ------------------------------------------------------------------------ */\n\nfunction listingOutcome(\n generation: AiValidatedGeneration<AiSeoFieldValues>,\n job: AiJob,\n subject: AiSeoFieldsProposal['subject'],\n context: {\n keywords: readonly string[]\n facts: SeoPageFacts | null\n fields: readonly SeoListingFieldKey[]\n hasImage: boolean\n },\n): AiJobStepOutcome {\n const spent = spentBy(generation)\n if (generation.status === 'refused') return { outputs: [], ...spent, refused: true }\n if (generation.status === 'needs_input') return { outputs: [], ...spent, failure: generation.message }\n const values = generation.value\n const notes: string[] = []\n if (context.fields.includes('imageAlt') && context.hasImage && !values.imageAlt) {\n notes.push('No image description was proposed: nothing on the page says what the picture shows.')\n }\n const proposal: AiSeoFieldsProposal = {\n kind: 'fields',\n subject,\n values,\n keywords: seoKeywordCoverage(context.keywords, {\n title: values.title,\n description: values.description,\n h1: context.facts?.h1s[0]?.text,\n body: context.facts?.text,\n }),\n notes,\n }\n const output: AiJobOutput = {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.fields(subject.kind, subject.id),\n hostId: job.hostId ?? null,\n label: `SEO proposal · ${subject.name}`,\n proposal: plain(proposal),\n }\n return { outputs: [output], ...spent }\n}\n\nasync function runScreenListing(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const screenId = str(job.inputs?.['screenId'])\n const snapshot = screenId ? await hostRef.collection('screens').doc(screenId).get() : null\n const screen = snapshot?.exists ? (snapshot.data() as SeoScreenDocument) : null\n if (!screen || screen.deletedAt) {\n return { outputs: [], ...spentNothing(model), failure: AI_SEO_NO_PAGE_COPY }\n }\n\n const versionId = str(job.inputs?.['versionId']) || str(screen.versionId)\n const version = await readVersionNodes(hostRef, screenId, versionId)\n const facts = seoPageFacts(version?.nodes as never, { rootId: version?.rootId })\n const fields = aiSeoRequestedFields(job.inputs?.['fields'], SCREEN_SEO_LISTING_FIELDS)\n const seo = screen.seo ?? {}\n const hasImage = Boolean(str(seo.image))\n // The share image is often a picture the page shows too: its description\n // and the text beside it there are what is known about it.\n const onPage = hasImage ? facts.images.find((image) => image.src === str(seo.image)) : undefined\n const route = host.screens?.[screenId]\n const path = typeof route === 'string' ? screenRoutePathToUrl(route) : null\n const name = str(screen.displayName) || path || 'Untitled page'\n const keywords = seoKeywordList(job.inputs?.['keywords'])\n const generation = await generateSeoFields({\n subject: { kind: 'screen', name, path },\n brand: brandOf(host),\n text: facts.text || str(screen.description),\n fields,\n current: {\n title: str(seo.title),\n description: str(seo.description),\n breadcrumb: str(seo.breadcrumb),\n imageAlt: str(seo.imageAlt),\n },\n image: hasImage ? { alt: str(seo.imageAlt) || onPage?.alt || null, context: onPage?.context || null } : null,\n keywords,\n otherTitles: await siteTitles(hostRef, new Set([screenId])),\n model,\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIELDS_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n })\n return listingOutcome(generation, job, { kind: 'screen', id: screenId, name, path }, {\n keywords,\n facts,\n fields,\n hasImage,\n })\n}\n\nasync function runProductListing(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, signal } = context\n const model = modelOf(context)\n const name = str(job.inputs?.['name']).slice(0, 200)\n if (!name) return { outputs: [], ...spentNothing(model), failure: AI_SEO_NO_PRODUCT_COPY }\n const fields = aiSeoRequestedFields(job.inputs?.['fields'], PRODUCT_LISTING_FIELDS)\n const keywords = seoKeywordList(job.inputs?.['keywords'])\n const productId = str(job.inputs?.['productId']) || null\n const generation = await generateSeoFields({\n subject: { kind: 'product', name },\n brand: brandOf(host),\n // The product's own words, as its editor handed them over: the product\n // document is the commerce plugin's, and this step never reads it.\n text: str(job.inputs?.['text']).slice(0, SEO_PAGE_TEXT_MAX_CHARS),\n fields,\n current: {\n title: str(job.inputs?.['currentTitle']),\n description: str(job.inputs?.['currentDescription']),\n },\n image: null,\n keywords,\n model,\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIELDS_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n })\n return listingOutcome(generation, job, { kind: 'product', id: productId, name }, {\n keywords,\n facts: null,\n fields,\n hasImage: false,\n })\n}\n\n/* ------------------------------------------------------------------------ *\n * The site audit\n * ------------------------------------------------------------------------ */\n\n/**\n * Read the site for an audit: the platform SEO check's scan\n * (`@aglyn/aglyn/app-utils/seo-site-scan`), which the site's SEO section\n * reads through too, so a job audits exactly the pages the check lists.\n */\nasync function scanSite(\n firestore: Firestore,\n hostId: string,\n host: SeoHostDocument,\n keywords: unknown,\n): Promise<SeoSiteScan> {\n return scanSeoSite(firestore, hostId, host, {\n keywords,\n templateScreenIds: await getTemplateScreenIds({ hostId }),\n })\n}\n\n/** One unit of generated work an audit still owes. */\nexport type AiSeoAuditUnit =\n | { kind: 'site' }\n | { kind: 'batch'; batch: number; of: number; screenIds: string[] }\n\n/** The units an audit owes after the outputs recorded so far, in the order they run. */\nexport function aiSeoPendingUnits(\n report: AiSeoAuditReport,\n outputs: readonly Pick<AiJobOutput, 'id'>[],\n): AiSeoAuditUnit[] {\n const done = new Set(outputs.map((output) => output.id))\n const units: AiSeoAuditUnit[] = []\n if (report.siteProposal && !done.has(AI_SEO_OUTPUT_IDS.site)) units.push({ kind: 'site' })\n const batches = aiSeoAuditBatches(report)\n batches.forEach((screenIds, index) => {\n if (!done.has(AI_SEO_OUTPUT_IDS.fixes(index + 1))) {\n units.push({ kind: 'batch', batch: index + 1, of: batches.length, screenIds })\n }\n })\n return units\n}\n\ninterface UnitResult {\n output: AiJobOutput\n spent: Spent\n}\n\n/* ---- The site-wide proposal ---------------------------------------------- */\n\nexport const AI_SEO_SITE_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose two things for a website: the structured data that says who publishes it, and ' +\n 'the guidance an AI agent reads first in its /llms.txt.\\n\\n' +\n `Answer by calling ${AI_SEO_SITE_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n '- Fill only the fields you are told are empty; answer null for every other one.\\n' +\n '- Use only what the pages you are given state. A contact email or telephone number goes in ' +\n 'only when a page shows it exactly; never compose one.\\n' +\n '- The entity description says what the publisher is, in one plain sentence, not what one page ' +\n 'offers.\\n' +\n '- The agent guidance is checkable and specific: the questions this site answers and which ' +\n 'pages answer them. No marketing language: an agent discounts a claim it cannot check.\\n' +\n '- Keep every length the tool states, and write in the language of the pages.',\n cacheBreakpoint: true,\n },\n]\n\n/** The site SEO form fields blank on the site now. */\nexport function aiSeoBlankSiteFields(host: SeoHostDocument): Set<AiSeoSiteFormField> {\n const entity = host.seo?.entity ?? {}\n const agent = host.seo?.agent ?? {}\n const blank = new Set<AiSeoSiteFormField>()\n if (entity.type === undefined || entity.type === null || entity.type === '') blank.add('seo.entity.type')\n if (!str(entity.name)) blank.add('seo.entity.name')\n if (!str(entity.description)) blank.add('seo.entity.description')\n if (!str(entity.email)) blank.add('seo.entity.email')\n if (!str(entity.telephone)) blank.add('seo.entity.telephone')\n if (!str(entity.contactType)) blank.add('seo.entity.contactType')\n if (!str(agent.whenToUse)) blank.add('seo.agent.whenToUse')\n if (!str(agent.howToUse)) blank.add('seo.agent.howToUse')\n return blank\n}\n\n/** The site's content collections, for the llms.txt preview. */\nasync function readContentCollections(hostRef: DocumentReference): Promise<LlmsTxtCollection[]> {\n try {\n const snapshot = await hostRef.collection('collections').select('slug', 'displayName', 'kind').limit(50).get()\n return snapshot.docs\n .filter((doc) => hostCollectionKind(doc.data()) === 'content' && str(doc.get('slug')))\n .map((doc) => ({ slug: str(doc.get('slug')), name: str(doc.get('displayName')) || undefined }))\n } catch {\n // A preview without the feeds is still a preview of the guidance.\n return []\n }\n}\n\n/** `/llms.txt` as it reads with `values` saved over what the site stores now. */\nexport function aiSeoLlmsPreview(\n host: SeoHostDocument,\n values: Partial<Record<AiSeoSiteFormField, string>>,\n pages: readonly LlmsTxtPage[],\n collections: readonly LlmsTxtCollection[],\n): string {\n return buildLlmsTxt({\n siteName: str(host.seo?.title) || str(host.displayName) || 'Site',\n origin: hostPublicOrigin(host) ?? '',\n description: str(host.seo?.description) || undefined,\n agent: {\n whenToUse: values['seo.agent.whenToUse'] ?? host.seo?.agent?.whenToUse,\n howToUse: values['seo.agent.howToUse'] ?? host.seo?.agent?.howToUse,\n },\n pages,\n collections,\n hasSearch: true,\n contactEmail: values['seo.entity.email'] ?? (str(host.seo?.entity?.email) || undefined),\n })\n}\n\nasync function proposeSite(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n scan: SeoSiteScan,\n): Promise<UnitResult> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const blank = aiSeoBlankSiteFields(host)\n const collections = await readContentCollections(hostRef)\n const topLevel: LlmsTxtPage[] = scan.pages\n .filter((page) => !page.path.replace(/^\\//, '').includes('/'))\n .slice(0, SITE_PAGES_LISTED)\n .map((page) => ({ path: page.path, ...(page.name ? { title: page.name } : {}) }))\n // The pages that say who publishes a site: the home page, and whatever is\n // about the company or how to reach it.\n const telling = scan.pages\n .filter((page) => page.path === '/' || /about|contact|company|team|who/i.test(page.path))\n .slice(0, 4)\n const prompt = [\n `Site: ${brandOf(host) || 'untitled site'}`,\n host.seo?.description ? `Site description: ${str(host.seo.description)}` : '',\n `Empty fields you may fill: ${[...blank].join(', ') || 'none'}`,\n 'Pages:',\n ...scan.pages\n .slice(0, SITE_PAGES_LISTED)\n .map((page) => `- ${page.path} — ${page.name}${str(page.seo.title) ? ` — ${str(page.seo.title)}` : ''}`),\n collections.length\n ? `Content collections: ${collections.map((entry) => entry.name || entry.slug).join(', ')}`\n : '',\n ...telling.flatMap((page) => ['', `Text of ${page.path}:`, page.facts.text.slice(0, 1_200) || '(no text)']),\n ]\n .filter((line) => line !== '')\n .join('\\n')\n const siteText = scan.pages.map((page) => page.facts.text).join('\\n')\n const generation = await runValidatedGeneration('seo-site', {\n step: 'job.seo',\n model,\n instructions: AI_SEO_SITE_INSTRUCTIONS,\n messages: [{ role: 'user', content: prompt }],\n tool: aiSeoSiteTool(),\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_SITE_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n check: (answer) => checkAiSeoSite(answer, { blank, siteText }),\n })\n const proposed = generation.status === 'ok' ? generation.value : { values: {}, notes: [] }\n const notes =\n generation.status === 'ok'\n ? proposed.notes\n : [\n generation.status === 'refused'\n ? 'The AI declined to propose structured data for this site.'\n : generation.message,\n ]\n const site: AiSeoSiteProposal = {\n values: proposed.values,\n llmsPreview: aiSeoLlmsPreview(host, proposed.values, topLevel, collections),\n notes,\n }\n return {\n output: {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.site,\n hostId: job.hostId ?? null,\n label: 'SEO audit · structured data and agent guidance',\n proposal: plain({ kind: 'site', site }),\n },\n spent: spentBy(generation),\n }\n}\n\n/* ---- A batch of page fixes ------------------------------------------------ */\n\nexport const AI_SEO_FIXES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You fix the search listings and headings of a website’s pages. For each page you are given ' +\n 'what an audit found, what the page shows now, and its text.\\n\\n' +\n `Answer by calling ${AI_SEO_FIXES_TOOL_NAME} exactly once, with one entry per page. A reply in ` +\n 'prose cannot be used.\\n\\n' +\n 'Rules:\\n' +\n '- Fix only what the audit found. Answer null for a value it did not find a problem with.\\n' +\n '- Write only from the page’s own text. Never invent a fact, a price, a name, a place or a claim.\\n' +\n '- A title says what the page is, specifically. No two pages share a title, and none repeats a ' +\n 'title you are told another page uses.\\n' +\n '- A description is one or two sentences telling a searcher what they will find.\\n' +\n '- The main heading names what the page is about in a few plain words.\\n' +\n '- An image description says what the picture most likely shows, from the text beside it. ' +\n 'Leave an image out when the text gives no idea.\\n' +\n '- A target keyword goes in only where the page is about it and it reads naturally: at most ' +\n 'once in a title and once in a description. Never list keywords or repeat a word to rank.\\n' +\n '- Keep every length the tool states, and write in the language of each page.',\n cacheBreakpoint: true,\n },\n]\n\n/** The fixes one page gets: the model's values, its content fixes, and the guidance no fix can give. */\nexport function aiSeoPageFixFor(page: AiSeoBatchPage, answer: AiSeoBatchAnswer | null): AiSeoPageFix {\n const values: AiSeoFieldValues = {}\n if (answer?.title) values.title = answer.title\n if (answer?.description) values.description = answer.description\n const content: AiSeoContentFix[] = []\n for (const alt of answer?.imageAlts ?? []) content.push({ kind: 'image-alt', nodeId: alt.nodeId, alt: alt.alt })\n if (answer?.h1) {\n const first = page.facts.h1s[0]\n const promotable = page.facts.headings.find((heading) => heading.editable && heading.nodeId)\n if (page.codes.has('h1-thin') && first?.editable && first.nodeId) {\n content.push({ kind: 'h1-set', nodeId: first.nodeId, text: answer.h1 })\n } else if (page.codes.has('h1-missing') && promotable?.nodeId) {\n content.push({ kind: 'h1-set', nodeId: promotable.nodeId, text: answer.h1 })\n } else if (page.codes.has('h1-missing')) {\n content.push({ kind: 'h1-insert', text: answer.h1 })\n }\n }\n if (page.codes.has('h1-multiple')) content.push(...aiSeoHeadingDemotions(page.facts))\n const guidance: string[] = []\n if (page.codes.has('orphan')) {\n guidance.push('Link to this page from your navigation or a related page. Where it belongs is yours to choose.')\n }\n return { screenId: page.screenId, values, content, guidance }\n}\n\n/** The user turn for one batch: each page's findings, what it shows now, and its text. */\nexport function aiSeoFixesPrompt(pages: readonly AiSeoBatchPage[], titlesElsewhere: readonly string[]): string {\n const titles = titlesElsewhere.length\n ? `Titles other pages use:\\n${titlesElsewhere.slice(0, 40).map((title) => `- ${title}`).join('\\n')}\\n\\n`\n : ''\n const blocks = pages.map((page) => {\n const lines = [\n `### Page ${page.screenId}: ${page.name} (${page.path})`,\n `Found: ${[...page.codes].map((code) => SEO_FINDING_LABELS[code]).join('; ')}`,\n `Title now: ${str(page.seo?.title) || '(none)'}`,\n `Description now: ${str(page.seo?.description) || '(none)'}`,\n `Main heading now: ${page.facts.h1s[0]?.text || '(none)'}`,\n ]\n if (page.keywords.length) lines.push(`Target keywords: ${page.keywords.join(', ')}`)\n const images = aiSeoFixImages(page)\n if (images.length) {\n lines.push(\n 'Images without a description:',\n ...images.map((image) => `- ${image.nodeId}: beside \"${image.context || 'no text'}\"`),\n )\n }\n lines.push('Text:', page.facts.text.slice(0, AI_SEO_BATCH_PAGE_TEXT_CHARS) || '(no text)')\n return lines.join('\\n')\n })\n return `${titles}${blocks.join('\\n\\n')}`\n}\n\nasync function proposeFixes(\n context: AiJobStepContext & { firestore: Firestore },\n report: AiSeoAuditReport,\n unit: Extract<AiSeoAuditUnit, { kind: 'batch' }>,\n): Promise<UnitResult> {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const hostRef = firestore.collection('hosts').doc(job.hostId as string)\n const reports = new Map(report.pages.map((page) => [page.screenId, page]))\n // Read now, not as audited: a page edited since keeps its edits, and one\n // deleted since is skipped.\n const loaded = await Promise.all(\n unit.screenIds.map(async (screenId): Promise<AiSeoBatchPage | null> => {\n const pageReport = reports.get(screenId)\n const snapshot = await hostRef.collection('screens').doc(screenId).get()\n const screen = snapshot.exists ? (snapshot.data() as SeoScreenDocument) : null\n if (!pageReport || !screen || screen.deletedAt) return null\n const version = await readVersionNodes(hostRef, screenId, str(screen.versionId))\n return {\n screenId,\n path: pageReport.path,\n name: pageReport.name,\n codes: new Set(pageReport.findings.map((entry) => entry.code)),\n keywords: pageReport.keywords.map((entry) => entry.keyword),\n seo: screen.seo ?? {},\n facts: seoPageFacts(version?.nodes as never, { rootId: version?.rootId }),\n }\n }),\n )\n const pages = loaded.filter((page): page is AiSeoBatchPage => page !== null)\n const titlesElsewhere = await siteTitles(hostRef, new Set(pages.map((page) => page.screenId)))\n\n let spent = spentNothing(model)\n let answers: Record<string, AiSeoBatchAnswer> = {}\n const notes: string[] = []\n if (pages.length) {\n const generation = await runValidatedGeneration('seo-fixes', {\n step: 'job.seo',\n model,\n instructions: AI_SEO_FIXES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiSeoFixesPrompt(pages, titlesElsewhere) }],\n tool: aiSeoFixesTool(pages.map((page) => page.screenId)),\n // Lowered on a tier too slow to answer a whole batch and ask again\n // inside the least time the step registers.\n maxTokens: aiSeoGenerationMaxTokens(model, AI_SEO_FIXES_MAX_TOKENS),\n ...(signal ? { signal } : {}),\n check: (answer) => checkAiSeoFixes(answer, pages, titlesElsewhere),\n })\n spent = spentBy(generation)\n if (generation.status === 'ok') {\n answers = generation.value\n } else {\n notes.push(\n generation.status === 'refused'\n ? 'The AI declined to propose fixes for these pages.'\n : generation.message,\n )\n }\n }\n const fixes = pages.map((page) => aiSeoPageFixFor(page, answers[page.screenId] ?? null))\n const skipped = unit.screenIds.length - pages.length\n if (skipped > 0) {\n notes.push(`${skipped} ${skipped === 1 ? 'page was' : 'pages were'} deleted since the audit and left out.`)\n }\n return {\n output: {\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.fixes(unit.batch),\n hostId: job.hostId ?? null,\n label: `SEO audit · fixes ${unit.batch} of ${unit.of}`,\n proposal: plain({ kind: 'fixes', batch: unit.batch, fixes, notes }),\n },\n spent,\n }\n}\n\nasync function runSiteAuditPass(\n context: AiJobStepContext & { firestore: Firestore },\n host: SeoHostDocument,\n): Promise<AiJobStepOutcome> {\n const { job, firestore } = context\n const model = modelOf(context)\n const recorded = aiSeoAuditView(job.outputs ?? [])\n const outputs: AiJobOutput[] = []\n let spent = spentNothing(model)\n let report: AiSeoAuditReport\n let scan: SeoSiteScan | null = null\n if (recorded) {\n report = recorded.report\n } else {\n scan = await scanSite(firestore, job.hostId as string, host, job.inputs?.['keywords'])\n report = aiSeoAudit(scan.pages, seoAuditSiteOf(host), { skipped: scan.skipped, notes: scan.notes })\n const findings = report.pages.reduce((sum, page) => sum + page.findings.length, report.site.length)\n outputs.push({\n resource: 'seo',\n id: AI_SEO_OUTPUT_IDS.report,\n hostId: job.hostId ?? null,\n label: `SEO audit · ${report.pages.length} ${report.pages.length === 1 ? 'page' : 'pages'} · ${findings} ${findings === 1 ? 'finding' : 'findings'}`,\n proposal: plain(report),\n })\n }\n const units = aiSeoPendingUnits(report, [...(job.outputs ?? []), ...outputs])\n if (!units.length) return { outputs, ...spent }\n const [unit] = units\n const result =\n unit.kind === 'site'\n ? await proposeSite(\n context,\n host,\n scan ?? (await scanSite(firestore, job.hostId as string, host, job.inputs?.['keywords'])),\n )\n : await proposeFixes(context, report, unit)\n outputs.push(result.output)\n spent = addSpent(spent, result.spent)\n return { outputs, ...spent, ...(units.length > 1 ? { continue: true } : {}) }\n}\n\nexport const runAiJobSeoStep: AiJobStepRunner = async (context) => {\n const { job, firestore } = context\n if (!firestore) {\n // The machine always hands one in; a runner called without it is a\n // wiring fault, not a customer path.\n throw new Error('the SEO step reads the site it writes about and was given no Firestore')\n }\n const target = aiSeoJobTarget(job.inputs)\n if (!target) return { outputs: [], ...spentNothing(modelOf(context)), failure: AI_SEO_NO_TARGET_COPY }\n const site = await loadSite(firestore, job)\n if ('failure' in site) return { outputs: [], ...spentNothing(modelOf(context)), failure: site.failure }\n const withFirestore = { ...context, firestore }\n switch (target) {\n case 'screen':\n return runScreenListing(withFirestore, site.host)\n case 'product':\n return runProductListing(withFirestore, site.host)\n case 'site':\n return runSiteAuditPass(withFirestore, site.host)\n }\n}\n"],"names":["hostCollectionKind","hostPublicOrigin","buildLlmsTxt","screenRoutePathToUrl","SEO_FINDING_LABELS","seoKeywordCoverage","seoKeywordList","SCREEN_SEO_LISTING_FIELDS","isSeoListingFieldKey","SEO_PAGE_TEXT_MAX_CHARS","seoPageFacts","scanSeoSite","seoAuditSiteOf","decodeStoredNodes","getTemplateScreenIds","AI_SEO_OUTPUT_IDS","aiSeoAuditView","aiSeoJobTarget","aiModelForStep","aiSeoAudit","aiSeoAuditBatches","aiSeoHeadingDemotions","runValidatedGeneration","AI_SEO_FIELDS_MAX_TOKENS","generateSeoFields","AI_SEO_FIXES_TOOL_NAME","AI_SEO_SITE_TOOL_NAME","aiSeoFixImages","aiSeoFixesTool","aiSeoSiteTool","checkAiSeoFixes","checkAiSeoSite","AI_SEO_FIXES_MAX_TOKENS","AI_SEO_SITE_MAX_TOKENS","aiSeoGenerationMaxTokens","AI_SEO_NO_SITE_COPY","AI_SEO_NO_PAGE_COPY","AI_SEO_NO_TARGET_COPY","AI_SEO_NO_PRODUCT_COPY","AI_SEO_BATCH_PAGE_TEXT_CHARS","SITE_PAGES_LISTED","PRODUCT_LISTING_FIELDS","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","round6","Math","round","modelOf","context","modelFor","spentNothing","model","usage","estCostUsd","stopReason","spentBy","generation","addSpent","a","b","plain","JSON","parse","stringify","aiSeoRequestedFields","raw","allowed","named","String","split","map","part","filter","wanted","Set","length","key","has","brandOf","host","seo","title","displayName","subdomain","loadSite","firestore","job","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","readVersionNodes","hostRef","screenId","versionId","nodes","rootId","siteTitles","except","select","limit","docs","id","Boolean","listingOutcome","subject","spent","status","outputs","refused","message","values","notes","fields","includes","hasImage","imageAlt","push","proposal","kind","keywords","description","h1","facts","h1s","text","body","output","resource","label","name","runScreenListing","screen","signal","inputs","deletedAt","version","image","onPage","images","find","src","undefined","route","screens","path","brand","current","breadcrumb","alt","otherTitles","maxTokens","runProductListing","slice","productId","scanSite","templateScreenIds","aiSeoPendingUnits","report","done","units","siteProposal","site","batches","forEach","screenIds","index","fixes","batch","of","AI_SEO_SITE_INSTRUCTIONS","cacheBreakpoint","aiSeoBlankSiteFields","entity","agent","blank","type","add","email","telephone","contactType","whenToUse","howToUse","readContentCollections","slug","aiSeoLlmsPreview","pages","collections","siteName","origin","hasSearch","contactEmail","proposeSite","scan","topLevel","page","replace","telling","test","prompt","join","entry","flatMap","line","siteText","step","instructions","messages","role","content","tool","check","answer","proposed","llmsPreview","AI_SEO_FIXES_INSTRUCTIONS","aiSeoPageFixFor","imageAlts","nodeId","first","promotable","headings","heading","editable","codes","guidance","aiSeoFixesPrompt","titlesElsewhere","titles","blocks","lines","code","proposeFixes","unit","reports","Map","loaded","Promise","all","pageReport","findings","keyword","answers","skipped","runSiteAuditPass","recorded","reduce","sum","result","continue","runAiJobSeoStep","Error","target","withFirestore"],"mappings":"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"}
@@ -150,6 +150,8 @@ export interface AiSiteBuiltRef {
150
150
  */
151
151
  export declare function aiSiteBuiltRefs(units: readonly AiSiteUnit[], outputs: readonly Pick<AiJobOutput, 'resource' | 'id' | 'label'>[]): Map<string, AiSiteBuiltRef>;
152
152
  type BuiltRefs = ReturnType<typeof aiSiteBuiltRefs>;
153
+ /** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */
154
+ export declare function aiSiteResolvedRef(ref: string | null, built: BuiltRefs): string | null;
153
155
  /**
154
156
  * The site's own words, as every unit's brief carries them.
155
157
  *
@@ -209,7 +209,7 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
209
209
  }
210
210
  return built;
211
211
  }
212
- /** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */ function resolved(ref, built) {
212
+ /** A plan reference as the unit's own job reads it: an id the site now has, or nothing. */ export function aiSiteResolvedRef(ref, built) {
213
213
  var _ref;
214
214
  var _built_get;
215
215
  if (!ref) return null;
@@ -313,7 +313,7 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
313
313
  unitPlan.reuse = plan.reuse.filter((entry)=>!placedByScreens.has(entry.id));
314
314
  unitPlan.create = [
315
315
  _extends({}, unit.creation, {
316
- duplicateOf: resolved(unit.creation.duplicateOf, built)
316
+ duplicateOf: aiSiteResolvedRef(unit.creation.duplicateOf, built)
317
317
  })
318
318
  ];
319
319
  brief.push(`Build the ${unit.creation.kind} “${unit.creation.name}”: ${unit.creation.why}`);
@@ -332,11 +332,11 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
332
332
  }
333
333
  unitPlan.screens = [
334
334
  _extends({}, screen, {
335
- layout: resolved(screen.layout, built),
336
- template: resolved(screen.template, built),
337
- duplicateOf: resolved(screen.duplicateOf, built),
335
+ layout: aiSiteResolvedRef(screen.layout, built),
336
+ template: aiSiteResolvedRef(screen.template, built),
337
+ duplicateOf: aiSiteResolvedRef(screen.duplicateOf, built),
338
338
  sections: screen.sections.map((section)=>_extends({}, section, {
339
- uses: section.uses.map((ref)=>resolved(ref, built)).filter((ref)=>!!ref)
339
+ uses: section.uses.map((ref)=>aiSiteResolvedRef(ref, built)).filter((ref)=>!!ref)
340
340
  }))
341
341
  })
342
342
  ];