@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.288

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 (228) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  3. package/src/lib/assets/site-styles/professional.webp +0 -0
  4. package/src/lib/assets/site-styles/store.webp +0 -0
  5. package/src/lib/assets/site-styles/trades.webp +0 -0
  6. package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
  7. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  8. package/src/lib/declarations.js +5 -2
  9. package/src/lib/declarations.js.map +1 -1
  10. package/src/lib/declarations.server.js +12 -0
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
  13. package/src/lib/jobs/ai-build-capabilities.js +27 -2
  14. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  15. package/src/lib/jobs/ai-business-profile-prefill.js +6 -3
  16. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
  17. package/src/lib/jobs/ai-image-reads.d.ts +59 -0
  18. package/src/lib/jobs/ai-image-reads.js +138 -0
  19. package/src/lib/jobs/ai-image-reads.js.map +1 -0
  20. package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
  21. package/src/lib/jobs/ai-job-build-step.js +83 -12
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
  25. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
  27. package/src/lib/jobs/ai-job-import-step.js +15 -11
  28. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
  30. package/src/lib/jobs/ai-job-insight-step.js +23 -5
  31. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-language.js +2 -1
  33. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  34. package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
  35. package/src/lib/jobs/ai-job-media-step.js +6 -8
  36. package/src/lib/jobs/ai-job-media-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
  38. package/src/lib/jobs/ai-job-navigation-step.js +7 -9
  39. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-page-language.js +7 -1
  41. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-sections.js +16 -3
  43. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  44. package/src/lib/jobs/ai-job-page-step.js +9 -1
  45. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
  47. package/src/lib/jobs/ai-job-plan-step.js +78 -13
  48. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
  50. package/src/lib/jobs/ai-job-privacy-step.js +14 -15
  51. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-products-step.js +5 -3
  53. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
  55. package/src/lib/jobs/ai-job-settings-step.js +7 -8
  56. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
  58. package/src/lib/jobs/ai-job-site-content.js +67 -8
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
  61. package/src/lib/jobs/ai-job-site-datasets.js +300 -30
  62. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-step.js +6 -3
  64. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
  66. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
  67. package/src/lib/jobs/ai-job-text-step.js +2 -1
  68. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  69. package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
  70. package/src/lib/jobs/ai-job-tracking-step.js +7 -8
  71. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
  73. package/src/lib/jobs/ai-jobs-beat.js +18 -8
  74. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  75. package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
  76. package/src/lib/jobs/ai-jobs-notify.js +136 -3
  77. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  78. package/src/lib/jobs/ai-layout-stock-photos.d.ts +155 -7
  79. package/src/lib/jobs/ai-layout-stock-photos.js +1517 -220
  80. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  81. package/src/lib/jobs/ai-product-image.d.ts +14 -0
  82. package/src/lib/jobs/ai-product-image.js +73 -21
  83. package/src/lib/jobs/ai-product-image.js.map +1 -1
  84. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
  85. package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
  86. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  87. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
  88. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
  89. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
  90. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
  91. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
  92. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
  93. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
  94. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
  95. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
  96. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.d.ts +120 -0
  97. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js +410 -0
  98. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js.map +1 -0
  99. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
  100. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
  101. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
  102. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
  103. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
  104. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
  105. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.d.ts +26 -0
  106. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js +714 -0
  107. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js.map +1 -0
  108. package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
  109. package/src/lib/layout-language/ai-layout-compiler.js +194 -18
  110. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  111. package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
  112. package/src/lib/layout-language/ai-layout-copy.js +383 -3
  113. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  114. package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
  115. package/src/lib/layout-language/ai-layout-design.js +31 -11
  116. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  117. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
  118. package/src/lib/layout-language/ai-layout-listings.js +9 -1
  119. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  120. package/src/lib/layout-language/ai-layout-pictures.d.ts +39 -1
  121. package/src/lib/layout-language/ai-layout-pictures.js +141 -4
  122. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  123. package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
  124. package/src/lib/layout-language/ai-layout-storefront.js +100 -20
  125. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
  126. package/src/lib/model/ai-build-job.d.ts +7 -0
  127. package/src/lib/model/ai-build-job.js +36 -7
  128. package/src/lib/model/ai-build-job.js.map +1 -1
  129. package/src/lib/model/ai-build-plan.d.ts +28 -4
  130. package/src/lib/model/ai-build-plan.js +49 -9
  131. package/src/lib/model/ai-build-plan.js.map +1 -1
  132. package/src/lib/model/ai-build-progress.js +3 -1
  133. package/src/lib/model/ai-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-job-notice.d.ts +8 -0
  135. package/src/lib/model/ai-job-notice.js +33 -0
  136. package/src/lib/model/ai-job-notice.js.map +1 -1
  137. package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
  138. package/src/lib/model/ai-job-plan-lines.js +6 -1
  139. package/src/lib/model/ai-job-plan-lines.js.map +1 -1
  140. package/src/lib/model/ai-jobs.types.d.ts +8 -0
  141. package/src/lib/model/ai-jobs.types.js.map +1 -1
  142. package/src/lib/model/ai-permissions.d.ts +13 -0
  143. package/src/lib/model/ai-permissions.js +29 -1
  144. package/src/lib/model/ai-permissions.js.map +1 -1
  145. package/src/lib/model/ai-plan-said-sections.d.ts +12 -3
  146. package/src/lib/model/ai-plan-said-sections.js +22 -5
  147. package/src/lib/model/ai-plan-said-sections.js.map +1 -1
  148. package/src/lib/model/ai-products.d.ts +18 -0
  149. package/src/lib/model/ai-products.js +32 -0
  150. package/src/lib/model/ai-products.js.map +1 -1
  151. package/src/lib/model/ai-site-context.d.ts +6 -2
  152. package/src/lib/model/ai-site-context.js +8 -2
  153. package/src/lib/model/ai-site-context.js.map +1 -1
  154. package/src/lib/model/ai-site-inspiration.d.ts +3 -2
  155. package/src/lib/model/ai-site-inspiration.js +3 -2
  156. package/src/lib/model/ai-site-inspiration.js.map +1 -1
  157. package/src/lib/plugin.js +4 -13
  158. package/src/lib/plugin.js.map +1 -1
  159. package/src/lib/providers/anthropic.js +9 -2
  160. package/src/lib/providers/anthropic.js.map +1 -1
  161. package/src/lib/providers/catalog.js +14 -5
  162. package/src/lib/providers/catalog.js.map +1 -1
  163. package/src/lib/providers/contract.d.ts +24 -1
  164. package/src/lib/providers/contract.js.map +1 -1
  165. package/src/lib/providers/routing.js +5 -2
  166. package/src/lib/providers/routing.js.map +1 -1
  167. package/src/lib/runtime/ai-dataset-generation.d.ts +120 -1
  168. package/src/lib/runtime/ai-dataset-generation.js +352 -3
  169. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  170. package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
  171. package/src/lib/runtime/ai-dev-replay.js +4 -2
  172. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  173. package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
  174. package/src/lib/runtime/ai-doctrine-validators.js +345 -28
  175. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  176. package/src/lib/runtime/ai-doctrine.d.ts +5 -4
  177. package/src/lib/runtime/ai-doctrine.js +10 -3
  178. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  179. package/src/lib/runtime/ai-icon-library.d.ts +27 -1
  180. package/src/lib/runtime/ai-icon-library.js +30 -3
  181. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  182. package/src/lib/runtime/ai-products-generation.d.ts +11 -1
  183. package/src/lib/runtime/ai-products-generation.js +51 -4
  184. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  185. package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
  186. package/src/lib/runtime/assist-answer-cache.js +5 -2
  187. package/src/lib/runtime/assist-answer-cache.js.map +1 -1
  188. package/src/lib/server/ai-allotments.js +7 -2
  189. package/src/lib/server/ai-allotments.js.map +1 -1
  190. package/src/lib/server/ai-experiment-versions.js +5 -5
  191. package/src/lib/server/ai-experiment-versions.js.map +1 -1
  192. package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
  193. package/src/lib/server/ai-host-permissions-roster.js +33 -0
  194. package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
  195. package/src/lib/server/ai-host-permissions.js +8 -6
  196. package/src/lib/server/ai-host-permissions.js.map +1 -1
  197. package/src/lib/server/ai-image-reads-route.d.ts +17 -0
  198. package/src/lib/server/ai-image-reads-route.js +106 -0
  199. package/src/lib/server/ai-image-reads-route.js.map +1 -0
  200. package/src/lib/server/ai-seo-apply.js +5 -5
  201. package/src/lib/server/ai-seo-apply.js.map +1 -1
  202. package/src/lib/server/ai-site-import-route.js +7 -4
  203. package/src/lib/server/ai-site-import-route.js.map +1 -1
  204. package/src/lib/server/assist-build.d.ts +15 -1
  205. package/src/lib/server/assist-build.js +30 -3
  206. package/src/lib/server/assist-build.js.map +1 -1
  207. package/src/lib/server/assist-chat.d.ts +11 -7
  208. package/src/lib/server/assist-chat.js +185 -48
  209. package/src/lib/server/assist-chat.js.map +1 -1
  210. package/src/lib/server/assist-docs-query.d.ts +51 -0
  211. package/src/lib/server/assist-docs-query.js +57 -0
  212. package/src/lib/server/assist-docs-query.js.map +1 -0
  213. package/src/lib/server/assist-jobs-context.d.ts +102 -0
  214. package/src/lib/server/assist-jobs-context.js +227 -0
  215. package/src/lib/server/assist-jobs-context.js.map +1 -0
  216. package/src/lib/server/assist-view-context.d.ts +8 -4
  217. package/src/lib/server/assist-view-context.js +12 -7
  218. package/src/lib/server/assist-view-context.js.map +1 -1
  219. package/src/lib/server.js +12 -0
  220. package/src/lib/server.js.map +1 -1
  221. package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
  222. package/src/lib/tools/ai-dataset-tool.js +24 -7
  223. package/src/lib/tools/ai-dataset-tool.js.map +1 -1
  224. package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
  225. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  226. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
  227. package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
  228. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
- import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
+ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
19
19
  import { ACCOUNTS_PLUGIN_ID } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
20
20
  import { HostScreenVisibility } from "@aglyn/aglyn/foundation/definitions/platform.types";
21
21
  import { FieldValue } from "firebase-admin/firestore";
@@ -71,7 +71,7 @@ function inputOf(job) {
71
71
  * include a store (Starter and up), the commerce plugin must run here, and the
72
72
  * member must be able to edit the site's pages.
73
73
  */ export async function aiSiteStorePagesRefusal(context, deps = {}) {
74
- var _context_org, _deps_admission, _context_org1, _host_get;
74
+ var _context_org, _deps_admission, _context_org1;
75
75
  const { job, firestore } = context;
76
76
  if (!job.hostId) return 'Open the site first.';
77
77
  if (!checkEntitlement((_context_org = context.org) != null ? _context_org : null, 'commerce')) return 'Your plan does not include a store.';
@@ -93,7 +93,7 @@ function inputOf(job) {
93
93
  });
94
94
  if (refused) return refused.error;
95
95
  const host = await firestore.collection('hosts').doc(job.hostId).get();
96
- return hostRoleCanWrite(((_host_get = host.get('memberRoles')) != null ? _host_get : {})[job.createdBy]) ? null : 'Editing requires the editor role';
96
+ return holdsSiteKey(host, job.createdBy, 'site.edit') ? null : siteKeyRefusal('site.edit');
97
97
  }
98
98
  /** The store pages' pass: every page written, user accounts on, one output a page. */ export function createAiSiteStorePagesRunner(deps = {}) {
99
99
  var _deps_write, _deps_writeSeo;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-store-pages.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { ACCOUNTS_PLUGIN_ID } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { HostScreenVisibility } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n aiStorePageNodes,\n aiStorePagesToWrite,\n AI_STORE_PAGES,\n type AiStorePageFacts,\n type AiStorePagePlan,\n} from '../model/ai-site-store-pages'\nimport { aiModelForStep } from '../providers/routing'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { writeAiDraft, writeAiDraftScreenSeo } from './ai-job-drafts'\nimport { aiUnspentOutcome } from './ai-job-generation'\nimport { aiPluginDraftAdmissionRefusal } from './ai-job-plugin-drafts'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The store pages' unit of a site start (AGL-3676): the account, cart and\n * policy pages a paid store gets beside its planned pages\n * (`model/ai-site-store-pages.ts`), written by code in one pass that asks no\n * model and costs no credits.\n *\n * Each page is a draft written through the same writer, under the same pages\n * allowance, as every page the start builds (`writeAiDraft`), under an id\n * derived from the unit's job, so a pass run again finds its own drafts. The\n * guided start's publish then puts them live with the rest.\n *\n * The site's user accounts are turned on here too: the Customer account's\n * sign-in, and the platform's /signin and /signup, answer only where the site\n * serves accounts (`accounts` is off by default on a site that predates\n * 2026-08; every new site is born with it). A site whose owner switched them\n * off keeps them off: an explicit deny is theirs.\n */\n\n/** The unit input a store pages unit carries what it writes in. */\nexport const AI_SITE_STORE_PAGES_INPUT = 'storePages'\n\n/** What a store pages unit is told: the store pages the plan implies, and what their words link. */\nexport interface AiSiteStorePagesInput {\n pages: AiStorePagePlan[]\n facts: AiStorePageFacts\n /** The layout the start built, which the pages render inside; the site's otherwise. */\n layoutId: string | null\n}\n\n/** The commerce plugin's name, in the refusal that asks for it. */\nconst STORE_LABEL = 'Commerce'\n\n/** What a store pages row says where nothing could be written: our failure. */\nexport const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.'\n\n/** The id of the store page a unit writes for `key`. */\nexport function aiSiteStorePageId(unitJobId: string, key: string): string {\n return `${unitJobId}-${key}`\n}\n\nconst PATH = /^\\/[a-z0-9-]*$/\n\nfunction inputOf(job: Pick<AiJob, 'inputs'>): AiSiteStorePagesInput | null {\n const raw = job.inputs?.[AI_SITE_STORE_PAGES_INPUT] as Partial<AiSiteStorePagesInput> | undefined\n if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null\n const keys = new Set(AI_STORE_PAGES.map((page) => page.key))\n const pages = raw.pages.filter((page) => page && keys.has(page.key) && typeof page.slug === 'string')\n const path = (value: unknown, fallback: string) => (typeof value === 'string' && PATH.test(value) ? value : fallback)\n return {\n pages,\n facts: {\n contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,\n shopPath: path(raw.facts.shopPath, '/shop'),\n shippingPath: path(raw.facts.shippingPath, '/shipping-returns'),\n },\n layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null,\n }\n}\n\n/**\n * Whether the store pages may be added for this member on this site, asked\n * before their pass so a refusal spends nothing; `null` admits. The plan must\n * include a store (Starter and up), the commerce plugin must run here, and the\n * member must be able to edit the site's pages.\n */\nexport async function aiSiteStorePagesRefusal(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy'> },\n deps: { admission?: typeof aiPluginDraftAdmissionRefusal } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (!checkEntitlement((context.org ?? null) as never, 'commerce')) return 'Your plan does not include a store.'\n const refused = await (deps.admission ?? aiPluginDraftAdmissionRefusal)(\n { firestore, orgId: job.orgId, hostId: job.hostId, inputs: {}, org: context.org ?? null },\n { kind: 'site', drafts: [{ resource: 'product', label: STORE_LABEL }], now: context.now },\n )\n if (refused) return refused.error\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n}\n\nexport interface AiSiteStorePagesRunnerDeps {\n write?: typeof writeAiDraft\n writeSeo?: typeof writeAiDraftScreenSeo\n}\n\n/** The store pages' pass: every page written, user accounts on, one output a page. */\nexport function createAiSiteStorePagesRunner(deps: AiSiteStorePagesRunnerDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiDraft\n const writeSeo = deps.writeSeo ?? writeAiDraftScreenSeo\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, now } = context\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const input = inputOf(job)\n const hostId = job.hostId\n if (!input || !hostId) return { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n const outputs: AiJobOutput[] = []\n let refusal: string | null = null\n for (const page of aiStorePagesToWrite(input.pages)) {\n const definition = AI_STORE_PAGES.find((one) => one.key === page.key)\n if (!definition) continue\n const id = aiSiteStorePageId(job.$id, page.key)\n const draft = await write(firestore, {\n kind: 'screen',\n hostId,\n id,\n uid: job.createdBy,\n org: context.org ?? null,\n name: definition.title,\n slug: page.slug,\n nodes: aiStorePageNodes(page.key, input.facts) as unknown as NodesMap,\n layoutId: input.layoutId,\n // The account and the cart are a shopper's own pages: unlisted, noindex (AGL-3676).\n ...(definition.noindex ? { visibility: HostScreenVisibility.UNLISTED } : {}),\n aiJobId: aiOriginJobId(job),\n now,\n })\n if (draft.ok === false) {\n // The pages allowance is full, or the site is gone: what was written stays.\n refusal = draft.error\n break\n }\n await writeSeo(firestore, { hostId, id, seo: definition.seo, now })\n outputs.push({\n resource: 'screen',\n id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n proposal: { storePage: page.key, path: page.href },\n })\n }\n if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now)\n if (!outputs.length) {\n return refusal\n ? aiUnspentOutcome(model, { review: { reason: 'limit', message: refusal, findings: [] } })\n : { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n }\n return aiUnspentOutcome(model, { outputs })\n }\n}\n\nexport const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner()\n\n/**\n * Turns on the site's user accounts (`accounts`, default-off per site), so\n * the account page's sign-in and the platform's /signin and /signup answer.\n * An owner's explicit switch-off is left as it is.\n */\nexport async function turnOnSiteAccounts(firestore: FirebaseFirestore.Firestore, hostId: string, now: Date): Promise<boolean> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return false\n const disabled = (host.get('disabledPlugins') ?? []) as unknown[]\n const enabled = (host.get('enabledPlugins') ?? []) as unknown[]\n if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false\n await hostRef.update({ enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID), updatedAt: now })\n return true\n}\n"],"names":["checkEntitlement","hostRoleCanWrite","ACCOUNTS_PLUGIN_ID","HostScreenVisibility","FieldValue","aiStorePageNodes","aiStorePagesToWrite","AI_STORE_PAGES","aiModelForStep","aiOriginJobId","writeAiDraft","writeAiDraftScreenSeo","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","AI_SITE_STORE_PAGES_INPUT","STORE_LABEL","AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY","aiSiteStorePageId","unitJobId","key","PATH","inputOf","job","raw","inputs","Array","isArray","pages","facts","keys","Set","map","page","filter","has","slug","path","value","fallback","test","contactPath","shopPath","shippingPath","layoutId","aiSiteStorePagesRefusal","context","deps","host","firestore","hostId","org","refused","admission","orgId","kind","drafts","resource","label","now","error","collection","doc","get","createdBy","createAiSiteStorePagesRunner","write","writeSeo","model","modelFor","input","failure","outputs","refusal","definition","find","one","id","$id","draft","uid","name","title","nodes","noindex","visibility","UNLISTED","aiJobId","ok","seo","push","versionId","hostSubdomain","proposal","storePage","href","length","turnOnSiteAccounts","review","reason","message","findings","runAiSiteStorePagesUnit","hostRef","exists","disabled","enabled","includes","update","enabledPlugins","arrayUnion","updatedAt"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AAC3E,SAASC,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,kBAAkB,QAAQ,8CAA6C;AAChF,SAASC,oBAAoB,QAAQ,qDAAoD;AAEzF,SAASC,UAAU,QAAQ,2BAA0B;AAErD,SACEC,gBAAgB,EAChBC,mBAAmB,EACnBC,cAAc,QAGT,kCAA8B;AACrC,SAASC,cAAc,QAAQ,0BAAsB;AACrD,SAASC,aAAa,QAAQ,wBAAoB;AAClD,SAASC,YAAY,EAAEC,qBAAqB,QAAQ,qBAAiB;AACrE,SAASC,gBAAgB,QAAQ,yBAAqB;AACtD,SAASC,6BAA6B,QAAQ,4BAAwB;AAGtE;;;;;;;;;;;;;;;;CAgBC,GAED,iEAAiE,GACjE,OAAO,MAAMC,4BAA4B,aAAY;AAUrD,iEAAiE,GACjE,MAAMC,cAAc;AAEpB,6EAA6E,GAC7E,OAAO,MAAMC,uCAAuC,oEAAmE;AAEvH,sDAAsD,GACtD,OAAO,SAASC,kBAAkBC,SAAiB,EAAEC,GAAW;IAC9D,OAAO,GAAGD,UAAU,CAAC,EAAEC,KAAK;AAC9B;AAEA,MAAMC,OAAO;AAEb,SAASC,QAAQC,GAA0B;QAC7BA;IAAZ,MAAMC,OAAMD,cAAAA,IAAIE,MAAM,qBAAVF,WAAY,CAACR,0BAA0B;IACnD,IAAI,CAACS,OAAO,CAACE,MAAMC,OAAO,CAACH,IAAII,KAAK,KAAK,CAACJ,IAAIK,KAAK,EAAE,OAAO;IAC5D,MAAMC,OAAO,IAAIC,IAAIvB,eAAewB,GAAG,CAAC,CAACC,OAASA,KAAKb,GAAG;IAC1D,MAAMQ,QAAQJ,IAAII,KAAK,CAACM,MAAM,CAAC,CAACD,OAASA,QAAQH,KAAKK,GAAG,CAACF,KAAKb,GAAG,KAAK,OAAOa,KAAKG,IAAI,KAAK;IAC5F,MAAMC,OAAO,CAACC,OAAgBC,WAAsB,OAAOD,UAAU,YAAYjB,KAAKmB,IAAI,CAACF,SAASA,QAAQC;IAC5G,OAAO;QACLX;QACAC,OAAO;YACLY,aAAajB,IAAIK,KAAK,CAACY,WAAW,GAAGJ,KAAKb,IAAIK,KAAK,CAACY,WAAW,EAAE,OAAO,OAAO;YAC/EC,UAAUL,KAAKb,IAAIK,KAAK,CAACa,QAAQ,EAAE;YACnCC,cAAcN,KAAKb,IAAIK,KAAK,CAACc,YAAY,EAAE;QAC7C;QACAC,UAAU,OAAOpB,IAAIoB,QAAQ,KAAK,YAAYpB,IAAIoB,QAAQ,GAAGpB,IAAIoB,QAAQ,GAAG;IAC9E;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,wBACpBC,OAAqH,EACrHC,OAA6D,CAAC,CAAC;QAIxCD,cACAC,iBAC+CD,eAK7CE;IATzB,MAAM,EAAEzB,GAAG,EAAE0B,SAAS,EAAE,GAAGH;IAC3B,IAAI,CAACvB,IAAI2B,MAAM,EAAE,OAAO;IACxB,IAAI,CAACjD,kBAAkB6C,eAAAA,QAAQK,GAAG,YAAXL,eAAe,MAAgB,aAAa,OAAO;IAC1E,MAAMM,UAAU,MAAM,EAACL,kBAAAA,KAAKM,SAAS,YAAdN,kBAAkBjC,+BACvC;QAAEmC;QAAWK,OAAO/B,IAAI+B,KAAK;QAAEJ,QAAQ3B,IAAI2B,MAAM;QAAEzB,QAAQ,CAAC;QAAG0B,GAAG,GAAEL,gBAAAA,QAAQK,GAAG,YAAXL,gBAAe;IAAK,GACxF;QAAES,MAAM;QAAQC,QAAQ;YAAC;gBAAEC,UAAU;gBAAWC,OAAO1C;YAAY;SAAE;QAAE2C,KAAKb,QAAQa,GAAG;IAAC;IAE1F,IAAIP,SAAS,OAAOA,QAAQQ,KAAK;IACjC,MAAMZ,OAAO,MAAMC,UAAUY,UAAU,CAAC,SAASC,GAAG,CAACvC,IAAI2B,MAAM,EAAEa,GAAG;IACpE,OAAO7D,iBAAiB,EAAC8C,YAAAA,KAAKe,GAAG,CAAC,0BAATf,YAA2B,CAAC,EAAE,CAACzB,IAAIyC,SAAS,CAAC,IAAI,OAAO;AACnF;AAOA,oFAAoF,GACpF,OAAO,SAASC,6BAA6BlB,OAAmC,CAAC,CAAC;QAClEA,aACGA;IADjB,MAAMmB,SAAQnB,cAAAA,KAAKmB,KAAK,YAAVnB,cAAcpC;IAC5B,MAAMwD,YAAWpB,iBAAAA,KAAKoB,QAAQ,YAAbpB,iBAAiBnC;IAClC,OAAO,OAAOkC;;QACZ,MAAM,EAAEvB,GAAG,EAAE0B,SAAS,EAAEU,GAAG,EAAE,GAAGb;QAChC,MAAMsB,gBAAQtB,QAAQuB,QAAQ,oBAAhBvB,QAAQuB,QAAQ,MAAhBvB,SAAmB,8BAAerC,eAAe;QAC/D,MAAM6D,QAAQhD,QAAQC;QACtB,MAAM2B,SAAS3B,IAAI2B,MAAM;QACzB,IAAI,CAACoB,SAAS,CAACpB,QAAQ,OAAO,aAAKrC,iBAAiBuD;YAAQG,SAAStD;;QACrE,MAAMuD,UAAyB,EAAE;QACjC,IAAIC,UAAyB;QAC7B,KAAK,MAAMxC,QAAQ1B,oBAAoB+D,MAAM1C,KAAK,EAAG;gBAS5CkB;YARP,MAAM4B,aAAalE,eAAemE,IAAI,CAAC,CAACC,MAAQA,IAAIxD,GAAG,KAAKa,KAAKb,GAAG;YACpE,IAAI,CAACsD,YAAY;YACjB,MAAMG,KAAK3D,kBAAkBK,IAAIuD,GAAG,EAAE7C,KAAKb,GAAG;YAC9C,MAAM2D,QAAQ,MAAMb,MAAMjB,WAAW;gBACnCM,MAAM;gBACNL;gBACA2B;gBACAG,KAAKzD,IAAIyC,SAAS;gBAClBb,GAAG,GAAEL,eAAAA,QAAQK,GAAG,YAAXL,eAAe;gBACpBmC,MAAMP,WAAWQ,KAAK;gBACtB9C,MAAMH,KAAKG,IAAI;gBACf+C,OAAO7E,iBAAiB2B,KAAKb,GAAG,EAAEkD,MAAMzC,KAAK;gBAC7Ce,UAAU0B,MAAM1B,QAAQ;eAEpB8B,WAAWU,OAAO,GAAG;gBAAEC,YAAYjF,qBAAqBkF,QAAQ;YAAC,IAAI,CAAC;gBAC1EC,SAAS7E,cAAca;gBACvBoC;;YAEF,IAAIoB,MAAMS,EAAE,KAAK,OAAO;gBACtB,4EAA4E;gBAC5Ef,UAAUM,MAAMnB,KAAK;gBACrB;YACF;YACA,MAAMO,SAASlB,WAAW;gBAAEC;gBAAQ2B;gBAAIY,KAAKf,WAAWe,GAAG;gBAAE9B;YAAI;YACjEa,QAAQkB,IAAI,CAAC;gBACXjC,UAAU;gBACVoB;gBACAc,WAAWZ,MAAMY,SAAS;gBAC1BzC;gBACA0C,eAAeb,MAAMa,aAAa;gBAClClC,OAAOqB,MAAME,IAAI;gBACjBY,UAAU;oBAAEC,WAAW7D,KAAKb,GAAG;oBAAEiB,MAAMJ,KAAK8D,IAAI;gBAAC;YACnD;QACF;QACA,IAAIvB,QAAQwB,MAAM,EAAE,MAAMC,mBAAmBhD,WAAWC,QAAQS;QAChE,IAAI,CAACa,QAAQwB,MAAM,EAAE;YACnB,OAAOvB,UACH5D,iBAAiBuD,OAAO;gBAAE8B,QAAQ;oBAAEC,QAAQ;oBAASC,SAAS3B;oBAAS4B,UAAU,EAAE;gBAAC;YAAE,KACtF,aAAKxF,iBAAiBuD;gBAAQG,SAAStD;;QAC7C;QACA,OAAOJ,iBAAiBuD,OAAO;YAAEI;QAAQ;IAC3C;AACF;AAEA,OAAO,MAAM8B,0BAA0BrC,+BAA8B;AAErE;;;;CAIC,GACD,OAAO,eAAegC,mBAAmBhD,SAAsC,EAAEC,MAAc,EAAES,GAAS;QAItFX,WACDA;IAJjB,MAAMuD,UAAUtD,UAAUY,UAAU,CAAC,SAASC,GAAG,CAACZ;IAClD,MAAMF,OAAO,MAAMuD,QAAQxC,GAAG;IAC9B,IAAI,CAACf,KAAKwD,MAAM,EAAE,OAAO;IACzB,MAAMC,YAAYzD,YAAAA,KAAKe,GAAG,CAAC,8BAATf,YAA+B,EAAE;IACnD,MAAM0D,WAAW1D,aAAAA,KAAKe,GAAG,CAAC,6BAATf,aAA8B,EAAE;IACjD,IAAIyD,SAASE,QAAQ,CAACxG,uBAAuBuG,QAAQC,QAAQ,CAACxG,qBAAqB,OAAO;IAC1F,MAAMoG,QAAQK,MAAM,CAAC;QAAEC,gBAAgBxG,WAAWyG,UAAU,CAAC3G;QAAqB4G,WAAWpD;IAAI;IACjG,OAAO;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-store-pages.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { ACCOUNTS_PLUGIN_ID } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { HostScreenVisibility } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n aiStorePageNodes,\n aiStorePagesToWrite,\n AI_STORE_PAGES,\n type AiStorePageFacts,\n type AiStorePagePlan,\n} from '../model/ai-site-store-pages'\nimport { aiModelForStep } from '../providers/routing'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { writeAiDraft, writeAiDraftScreenSeo } from './ai-job-drafts'\nimport { aiUnspentOutcome } from './ai-job-generation'\nimport { aiPluginDraftAdmissionRefusal } from './ai-job-plugin-drafts'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The store pages' unit of a site start (AGL-3676): the account, cart and\n * policy pages a paid store gets beside its planned pages\n * (`model/ai-site-store-pages.ts`), written by code in one pass that asks no\n * model and costs no credits.\n *\n * Each page is a draft written through the same writer, under the same pages\n * allowance, as every page the start builds (`writeAiDraft`), under an id\n * derived from the unit's job, so a pass run again finds its own drafts. The\n * guided start's publish then puts them live with the rest.\n *\n * The site's user accounts are turned on here too: the Customer account's\n * sign-in, and the platform's /signin and /signup, answer only where the site\n * serves accounts (`accounts` is off by default on a site that predates\n * 2026-08; every new site is born with it). A site whose owner switched them\n * off keeps them off: an explicit deny is theirs.\n */\n\n/** The unit input a store pages unit carries what it writes in. */\nexport const AI_SITE_STORE_PAGES_INPUT = 'storePages'\n\n/** What a store pages unit is told: the store pages the plan implies, and what their words link. */\nexport interface AiSiteStorePagesInput {\n pages: AiStorePagePlan[]\n facts: AiStorePageFacts\n /** The layout the start built, which the pages render inside; the site's otherwise. */\n layoutId: string | null\n}\n\n/** The commerce plugin's name, in the refusal that asks for it. */\nconst STORE_LABEL = 'Commerce'\n\n/** What a store pages row says where nothing could be written: our failure. */\nexport const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.'\n\n/** The id of the store page a unit writes for `key`. */\nexport function aiSiteStorePageId(unitJobId: string, key: string): string {\n return `${unitJobId}-${key}`\n}\n\nconst PATH = /^\\/[a-z0-9-]*$/\n\nfunction inputOf(job: Pick<AiJob, 'inputs'>): AiSiteStorePagesInput | null {\n const raw = job.inputs?.[AI_SITE_STORE_PAGES_INPUT] as Partial<AiSiteStorePagesInput> | undefined\n if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null\n const keys = new Set(AI_STORE_PAGES.map((page) => page.key))\n const pages = raw.pages.filter((page) => page && keys.has(page.key) && typeof page.slug === 'string')\n const path = (value: unknown, fallback: string) => (typeof value === 'string' && PATH.test(value) ? value : fallback)\n return {\n pages,\n facts: {\n contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,\n shopPath: path(raw.facts.shopPath, '/shop'),\n shippingPath: path(raw.facts.shippingPath, '/shipping-returns'),\n },\n layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null,\n }\n}\n\n/**\n * Whether the store pages may be added for this member on this site, asked\n * before their pass so a refusal spends nothing; `null` admits. The plan must\n * include a store (Starter and up), the commerce plugin must run here, and the\n * member must be able to edit the site's pages.\n */\nexport async function aiSiteStorePagesRefusal(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy'> },\n deps: { admission?: typeof aiPluginDraftAdmissionRefusal } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (!checkEntitlement((context.org ?? null) as never, 'commerce')) return 'Your plan does not include a store.'\n const refused = await (deps.admission ?? aiPluginDraftAdmissionRefusal)(\n { firestore, orgId: job.orgId, hostId: job.hostId, inputs: {}, org: context.org ?? null },\n { kind: 'site', drafts: [{ resource: 'product', label: STORE_LABEL }], now: context.now },\n )\n if (refused) return refused.error\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return holdsSiteKey(host, job.createdBy, 'site.edit') ? null : siteKeyRefusal('site.edit')\n}\n\nexport interface AiSiteStorePagesRunnerDeps {\n write?: typeof writeAiDraft\n writeSeo?: typeof writeAiDraftScreenSeo\n}\n\n/** The store pages' pass: every page written, user accounts on, one output a page. */\nexport function createAiSiteStorePagesRunner(deps: AiSiteStorePagesRunnerDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiDraft\n const writeSeo = deps.writeSeo ?? writeAiDraftScreenSeo\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, now } = context\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const input = inputOf(job)\n const hostId = job.hostId\n if (!input || !hostId) return { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n const outputs: AiJobOutput[] = []\n let refusal: string | null = null\n for (const page of aiStorePagesToWrite(input.pages)) {\n const definition = AI_STORE_PAGES.find((one) => one.key === page.key)\n if (!definition) continue\n const id = aiSiteStorePageId(job.$id, page.key)\n const draft = await write(firestore, {\n kind: 'screen',\n hostId,\n id,\n uid: job.createdBy,\n org: context.org ?? null,\n name: definition.title,\n slug: page.slug,\n nodes: aiStorePageNodes(page.key, input.facts) as unknown as NodesMap,\n layoutId: input.layoutId,\n // The account and the cart are a shopper's own pages: unlisted, noindex (AGL-3676).\n ...(definition.noindex ? { visibility: HostScreenVisibility.UNLISTED } : {}),\n aiJobId: aiOriginJobId(job),\n now,\n })\n if (draft.ok === false) {\n // The pages allowance is full, or the site is gone: what was written stays.\n refusal = draft.error\n break\n }\n await writeSeo(firestore, { hostId, id, seo: definition.seo, now })\n outputs.push({\n resource: 'screen',\n id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n proposal: { storePage: page.key, path: page.href },\n })\n }\n if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now)\n if (!outputs.length) {\n return refusal\n ? aiUnspentOutcome(model, { review: { reason: 'limit', message: refusal, findings: [] } })\n : { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n }\n return aiUnspentOutcome(model, { outputs })\n }\n}\n\nexport const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner()\n\n/**\n * Turns on the site's user accounts (`accounts`, default-off per site), so\n * the account page's sign-in and the platform's /signin and /signup answer.\n * An owner's explicit switch-off is left as it is.\n */\nexport async function turnOnSiteAccounts(firestore: FirebaseFirestore.Firestore, hostId: string, now: Date): Promise<boolean> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return false\n const disabled = (host.get('disabledPlugins') ?? []) as unknown[]\n const enabled = (host.get('enabledPlugins') ?? []) as unknown[]\n if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false\n await hostRef.update({ enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID), updatedAt: now })\n return true\n}\n"],"names":["checkEntitlement","holdsSiteKey","siteKeyRefusal","ACCOUNTS_PLUGIN_ID","HostScreenVisibility","FieldValue","aiStorePageNodes","aiStorePagesToWrite","AI_STORE_PAGES","aiModelForStep","aiOriginJobId","writeAiDraft","writeAiDraftScreenSeo","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","AI_SITE_STORE_PAGES_INPUT","STORE_LABEL","AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY","aiSiteStorePageId","unitJobId","key","PATH","inputOf","job","raw","inputs","Array","isArray","pages","facts","keys","Set","map","page","filter","has","slug","path","value","fallback","test","contactPath","shopPath","shippingPath","layoutId","aiSiteStorePagesRefusal","context","deps","firestore","hostId","org","refused","admission","orgId","kind","drafts","resource","label","now","error","host","collection","doc","get","createdBy","createAiSiteStorePagesRunner","write","writeSeo","model","modelFor","input","failure","outputs","refusal","definition","find","one","id","$id","draft","uid","name","title","nodes","noindex","visibility","UNLISTED","aiJobId","ok","seo","push","versionId","hostSubdomain","proposal","storePage","href","length","turnOnSiteAccounts","review","reason","message","findings","runAiSiteStorePagesUnit","hostRef","exists","disabled","enabled","includes","update","enabledPlugins","arrayUnion","updatedAt"],"mappings":"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"}
@@ -58,7 +58,8 @@ import { runAiOverlayCopy } from "./ai-job-overlay-copy.js";
58
58
  * customer's brief and the org's inputs never enter it.
59
59
  */ export const AI_JOB_TEXT_SYSTEM = [
60
60
  {
61
- text: 'You write short, publishable copy for a website builder. You are ' + 'given a brief from the person who owns the site. Answer with the ' + 'copy only: no preamble, no headings unless the brief asks for them, ' + 'no closing remarks, no markdown fences. Keep to the length the brief ' + 'implies; when it implies none, write under 150 words. Write in the ' + 'language of the brief. Never invent facts, prices, names or claims ' + 'the brief did not give you; where a detail is missing, leave a ' + 'clearly marked placeholder in square brackets.',
61
+ text: 'You write short, publishable copy for a website builder. You are ' + 'given a brief from the person who owns the site. Answer with the ' + 'copy only: no preamble, no headings unless the brief asks for them, ' + 'no closing remarks, no markdown fences. Keep to the length the brief ' + 'implies; when it implies none, write under 150 words. Write in the ' + 'language of the brief. Never invent facts, prices, names or claims ' + // AGL-4092: never a bracketed placeholder, which a page shows as written.
62
+ 'the brief did not give you; where a detail is missing, leave it out ' + 'and write the sentence without it.',
62
63
  cacheBreakpoint: true
63
64
  }
64
65
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobInputRequest,\n AiJobItemFailure,\n AiJobItemLedger,\n AiJobItemStatus,\n AiJobSitePublish,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { aiDevEffortFor } from '../runtime/ai-dev-env'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport type { AiSiteImportState } from '../model/ai-site-import'\nimport type { AiSitePhotos } from '../model/ai-site-photos'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiOverlayCopyRequested } from '../model/ai-overlay-copy'\nimport { runAiOverlayCopy } from './ai-job-overlay-copy'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n /**\n * With a `plan` review: the plan needs an answer only the person can give\n * first (AGL-3978) — a tracking change with no pixel ID. The machine keeps\n * the plan and its review and parks the job `needs_input` with these\n * fields instead of `needs_review`; the answer returns it to the review.\n */\n input?: AiJobInputRequest\n /**\n * The answer was refused by the platform's own checks, not the model's\n * (AGL-3594): the machine meters the step as any other and then gives the\n * job's credits back to the meters it was charged to (`refundJobCredits`),\n * a bounded number of times a day.\n */\n uncredited?: boolean\n /**\n * The `failure` is the workspace's own limit, not a fault of ours: the\n * month's Free AI credits are spent (a 429 from the kind's admission, asked\n * when the plan is kept). The machine fails the job and gives the step's\n * credits back exactly as for our failure, but announces it as NOT ours, so\n * no staff alert is raised for a customer reaching their allowance.\n */\n customerLimit?: boolean\n /**\n * What a guided site start put live on its last pass (AGL-3596), kept on\n * the job by the machine in the write that records the pass: a step never\n * writes the job document itself.\n */\n sitePublish?: AiJobSitePublish\n /**\n * A `build` pass's unit and what it came to (AGL-3616), which the machine\n * writes to the job's item ledger in the transaction that records the\n * pass's spend. An item's failure is never the step's: it is a row, and\n * the build goes on.\n */\n item?: AiJobItemOutcome\n /** A `build`'s ledger to start from, written by the machine in the same transaction (AGL-3616). */\n items?: AiJobItemLedger[]\n /**\n * An `import` job's waves after this pass (AGL-3976), kept on the job by\n * the machine in the write that records the pass: a step never writes the\n * job document itself.\n */\n siteImport?: AiSiteImportState\n /**\n * A `site` job's pictures left on a starter photo after this pass\n * (AGL-3988), whole: what its late photo pass fills before the site is\n * published. A page's or a dataset's pass reports its own; the site step\n * merges them and the machine keeps the result on the job.\n */\n sitePhotos?: AiSitePhotos\n}\n\n/** What one `build` pass came to for the unit it ran (AGL-3616). */\nexport interface AiJobItemOutcome {\n slot: string\n /** `running` while the unit asks for another pass of its own. */\n status: AiJobItemStatus\n failure?: AiJobItemFailure | null\n /** Customer-safe: what the person should know about the item. */\n note?: string | null\n degradedBy?: string[]\n /** The ids of the outputs this pass produced for the item. */\n outputs?: string[]\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n 'the brief did not give you; where a detail is missing, leave a ' +\n 'clearly marked placeholder in square brackets.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n // Overlay copy (AGL-3603): the same one request, answered as fields.\n if (aiOverlayCopyRequested(job.inputs)) {\n return runAiOverlayCopy({\n job,\n model,\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n }\n // A development run's effort A/B arm (AGL-3781), else the row's.\n const effort = aiDevEffortFor('job.text') ?? AI_ROUTING_TABLE['job.text'].effort\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(effort ? { effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDevEffortFor","runAiRequest","aiJobStepBudget","aiOverlayCopyRequested","runAiOverlayCopy","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","effort","result","system","messages","role","content","thinking","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobInputRequest,\n AiJobItemFailure,\n AiJobItemLedger,\n AiJobItemStatus,\n AiJobSitePublish,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { aiDevEffortFor } from '../runtime/ai-dev-env'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport type { AiSiteImportState } from '../model/ai-site-import'\nimport type { AiSitePhotos } from '../model/ai-site-photos'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiOverlayCopyRequested } from '../model/ai-overlay-copy'\nimport { runAiOverlayCopy } from './ai-job-overlay-copy'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n /**\n * With a `plan` review: the plan needs an answer only the person can give\n * first (AGL-3978) — a tracking change with no pixel ID. The machine keeps\n * the plan and its review and parks the job `needs_input` with these\n * fields instead of `needs_review`; the answer returns it to the review.\n */\n input?: AiJobInputRequest\n /**\n * The answer was refused by the platform's own checks, not the model's\n * (AGL-3594): the machine meters the step as any other and then gives the\n * job's credits back to the meters it was charged to (`refundJobCredits`),\n * a bounded number of times a day.\n */\n uncredited?: boolean\n /**\n * The `failure` is the workspace's own limit, not a fault of ours: the\n * month's Free AI credits are spent (a 429 from the kind's admission, asked\n * when the plan is kept). The machine fails the job and gives the step's\n * credits back exactly as for our failure, but announces it as NOT ours, so\n * no staff alert is raised for a customer reaching their allowance.\n */\n customerLimit?: boolean\n /**\n * What a guided site start put live on its last pass (AGL-3596), kept on\n * the job by the machine in the write that records the pass: a step never\n * writes the job document itself.\n */\n sitePublish?: AiJobSitePublish\n /**\n * A `build` pass's unit and what it came to (AGL-3616), which the machine\n * writes to the job's item ledger in the transaction that records the\n * pass's spend. An item's failure is never the step's: it is a row, and\n * the build goes on.\n */\n item?: AiJobItemOutcome\n /** A `build`'s ledger to start from, written by the machine in the same transaction (AGL-3616). */\n items?: AiJobItemLedger[]\n /**\n * An `import` job's waves after this pass (AGL-3976), kept on the job by\n * the machine in the write that records the pass: a step never writes the\n * job document itself.\n */\n siteImport?: AiSiteImportState\n /**\n * A `site` job's pictures left on a starter photo after this pass\n * (AGL-3988), whole: what its late photo pass fills before the site is\n * published. A page's or a dataset's pass reports its own; the site step\n * merges them and the machine keeps the result on the job.\n */\n sitePhotos?: AiSitePhotos\n}\n\n/** What one `build` pass came to for the unit it ran (AGL-3616). */\nexport interface AiJobItemOutcome {\n slot: string\n /** `running` while the unit asks for another pass of its own. */\n status: AiJobItemStatus\n failure?: AiJobItemFailure | null\n /** Customer-safe: what the person should know about the item. */\n note?: string | null\n degradedBy?: string[]\n /** The ids of the outputs this pass produced for the item. */\n outputs?: string[]\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n // AGL-4092: never a bracketed placeholder, which a page shows as written.\n 'the brief did not give you; where a detail is missing, leave it out ' +\n 'and write the sentence without it.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n // Overlay copy (AGL-3603): the same one request, answered as fields.\n if (aiOverlayCopyRequested(job.inputs)) {\n return runAiOverlayCopy({\n job,\n model,\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n }\n // A development run's effort A/B arm (AGL-3781), else the row's.\n const effort = aiDevEffortFor('job.text') ?? AI_ROUTING_TABLE['job.text'].effort\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(effort ? { effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDevEffortFor","runAiRequest","aiJobStepBudget","aiOverlayCopyRequested","runAiOverlayCopy","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","effort","result","system","messages","role","content","thinking","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
@@ -63,7 +63,8 @@ export declare const AI_TRACKING_KIND: "tracking";
63
63
  /** ASSUMED: the least time the step needs — the site, the org and one transaction. */
64
64
  export declare const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5000;
65
65
  export declare const AI_TRACKING_NO_SITE_COPY = "Open the site whose tracking should change.";
66
- export declare const AI_TRACKING_ROLE_COPY = "Changing the site\u2019s tracking needs the admin, editor or author role on this site.";
66
+ /** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */
67
+ export declare const AI_TRACKING_ROLE_COPY: string;
67
68
  export declare const AI_TRACKING_LOCKED_COPY = "This site is not accepting changes right now.";
68
69
  export declare const AI_TRACKING_NOTHING_COPY = "The request named no tracking id this could set or remove.";
69
70
  /** Where a person finds what the op changed: the card that edits it. */
@@ -15,7 +15,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
18
- import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
+ import { holdsSiteKey } from "@aglyn/aglyn/app-utils/host-permissions";
19
+ import { orgPermissionLabel } from "@aglyn/aglyn/app-utils/org-permissions";
19
20
  import { SCREEN_ROOT_PATH } from "@aglyn/aglyn/app-utils/screen-route";
20
21
  import { GA_MEASUREMENT_ID_PATTERN, GOOGLE_ADS_ID_PATTERN, GTM_CONTAINER_ID_PATTERN, LINKEDIN_PARTNER_ID_PATTERN, META_PIXEL_ID_PATTERN, PINTEREST_TAG_ID_PATTERN, TIKTOK_PIXEL_ID_PATTERN } from "@aglyn/aglyn/app-utils/visitor-consent";
21
22
  import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
@@ -66,7 +67,7 @@ import { registerAiJobStep } from "./ai-jobs.js";
66
67
  */ export const AI_TRACKING_KIND = 'tracking';
67
68
  /** ASSUMED: the least time the step needs — the site, the org and one transaction. */ export const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5000;
68
69
  export const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.';
69
- export const AI_TRACKING_ROLE_COPY = 'Changing the site’s tracking needs the admin, editor or author role on this site.';
70
+ /** Names "Edit site content" (`site.edit`, AGL-4047), never a role. */ export const AI_TRACKING_ROLE_COPY = `Changing the site’s tracking needs the "${orgPermissionLabel('site.edit')}" permission on this site — ask an organization admin.`;
70
71
  export const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.';
71
72
  export const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.';
72
73
  /** Where a person finds what the op changed: the card that edits it. */ export const AI_TRACKING_WHERE = 'Setup → Tracking';
@@ -316,14 +317,13 @@ const getPath = (data, path)=>path.split('.').reduce((at, key)=>at && typeof at
316
317
  */ export async function writeAiSiteTracking(firestore, input) {
317
318
  const hostRef = firestore.collection('hosts').doc(input.hostId);
318
319
  return firestore.runTransaction(async (tx)=>{
319
- var _host_get, _host_data, _data_analytics, _data_consent;
320
+ var _host_data, _data_analytics, _data_consent;
320
321
  const host = await tx.get(hostRef);
321
322
  if (!host.exists) return {
322
323
  ok: false,
323
324
  error: 'Unknown site'
324
325
  };
325
- const roles = (_host_get = host.get('memberRoles')) != null ? _host_get : {};
326
- if (!hostRoleCanWrite(roles[input.uid])) return {
326
+ if (!holdsSiteKey(host, input.uid, 'site.edit')) return {
327
327
  ok: false,
328
328
  error: AI_TRACKING_ROLE_COPY
329
329
  };
@@ -386,10 +386,9 @@ const getPath = (data, path)=>path.split('.').reduce((at, key)=>at && typeof at
386
386
  }
387
387
  // ── The step ──────────────────────────────────────────────────────────────
388
388
  /** Why this member may not change this site's tracking now, or `null`. */ export async function aiTrackingAccessRefusal(input, deps = {}) {
389
- var _input_host_get, _deps_lockdown;
389
+ var _deps_lockdown;
390
390
  if (!input.host.exists) return 'Unknown site';
391
- const roles = (_input_host_get = input.host.get('memberRoles')) != null ? _input_host_get : {};
392
- if (!hostRoleCanWrite(roles[input.uid])) return AI_TRACKING_ROLE_COPY;
391
+ if (!holdsSiteKey(input.host, input.uid, 'site.edit')) return AI_TRACKING_ROLE_COPY;
393
392
  const locked = await ((_deps_lockdown = deps.lockdown) != null ? _deps_lockdown : getLockdownVerdict)({
394
393
  staff: false,
395
394
  uid: input.uid,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-tracking-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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n GA_MEASUREMENT_ID_PATTERN,\n GOOGLE_ADS_ID_PATTERN,\n GTM_CONTAINER_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n PINTEREST_TAG_ID_PATTERN,\n TIKTOK_PIXEL_ID_PATTERN,\n} from '@aglyn/aglyn/app-utils/visitor-consent'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { PluginAiCapabilityArgsSchema, PluginAiCapabilityInputField } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJobOutput } from '../model/ai-jobs.types'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobEditModel } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `tracking` step (AGL-3751): the site's analytics and advertising tags,\n * as the console's Setup → Tracking card sets them — Google Analytics, Google\n * Tag Manager, and the Meta, Google Ads, LinkedIn, TikTok and Pinterest tags —\n * as a first-class build operation, so \"connect Google Analytics G-4R7KX2PQ9Z\"\n * is one item on Assist's plan card.\n *\n * NO MODEL. The planner already wrote the ids as the item's arguments; this\n * holds each to the pattern the card's field (and the tenant's loader) holds\n * it to and writes it, so it costs no credits.\n *\n * ── Only the card's own fields, and only public ids ──────────────────────\n *\n * Every field written is one the Tracking card writes, under the same name:\n * `analytics.gaMeasurementId`, `analytics.gtmContainerId` and\n * `analytics.adTags.{meta,google-ads,linkedin,tiktok,pinterest}`, each tested\n * by the same expression (`GA_MEASUREMENT_ID_PATTERN` and its siblings in\n * `visitor-consent.ts`). Each is an id that is printed in every page's HTML\n * anyway, never a credential. Removing one deletes the field, as the card's\n * cleared box does (`CLEARABLE_TRACKING_PATHS`).\n *\n * NEVER: the consent banner (`consent.*` — turning it off, its mode, or the\n * advertising question), search indexing, verification tokens, and anything\n * that needs a secret the person pastes — the Ad conversions card's access\n * tokens, Zapier, order webhooks' signing secrets. Those are not arguments.\n * The tags load only where the consent banner lets them, exactly as a tag the\n * card saved does: the tenant gates on the stored ids, not on who wrote them.\n *\n * ── Live on confirm, who may, idempotent ─────────────────────────────────\n *\n * As the `settings` step (AGL-3748): a tag has no draft, so the plan card's\n * confirm is the agreement and the write is live. One transaction re-reads\n * the host, re-checks the role, writes only what differs with a site-wide\n * publish outbox entry, and the site's cached pages are dropped after. The\n * gate is the Firestore rules' for the host document (`canWriteHostContent`,\n * `hostWritesFrozen`): an admin, editor or author, while no lockdown freezes\n * the site, its org or the platform.\n */\n\nexport const AI_TRACKING_KIND = 'tracking' as const\n\n/** ASSUMED: the least time the step needs — the site, the org and one transaction. */\nexport const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5_000\n\nexport const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.'\nexport const AI_TRACKING_ROLE_COPY = 'Changing the site’s tracking needs the admin, editor or author role on this site.'\nexport const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.'\n\n/** Where a person finds what the op changed: the card that edits it. */\nexport const AI_TRACKING_WHERE = 'Setup → Tracking'\n\n/** The output's fixed id: one site's tracking, whatever changed. */\nexport const AI_TRACKING_OUTPUT_ID = 'tracking'\n\n/** One tag the card sets: its argument, its stored field, its name, and its pattern. */\nexport interface AiTrackingTag {\n arg: string\n path: string\n label: string\n pattern: RegExp\n /** What the card's field says the id looks like. */\n shape: string\n /** The id's name, as a field asking for it is labeled (AGL-3978): it names the provider. */\n field: string\n /** Asked when a request names this tag and no usable id, from the card's helper text. */\n ask: string\n /** Words that name this tag in an item's name. */\n named: RegExp\n}\n\n/** The Tracking card's fields, in its order (`trackingSchema`, host-settings-scope.tsx). */\nexport const AI_TRACKING_TAGS: readonly AiTrackingTag[] = [\n {\n arg: 'googleAnalytics',\n path: 'analytics.gaMeasurementId',\n label: 'Google Analytics',\n pattern: GA_MEASUREMENT_ID_PATTERN,\n shape: 'G-XXXXXXXXXX',\n field: 'Google Analytics measurement ID',\n ask: 'What’s your Google Analytics measurement ID? It looks like G-XXXXXXXXXX.',\n named: /google analytics|\\bga4?\\b|\\banalytics\\b/i,\n },\n {\n arg: 'googleTagManager',\n path: 'analytics.gtmContainerId',\n label: 'Google Tag Manager',\n pattern: GTM_CONTAINER_ID_PATTERN,\n shape: 'GTM-XXXXXXX',\n field: 'Google Tag Manager container ID',\n ask: 'What’s your Google Tag Manager container ID? It looks like GTM-XXXXXXX.',\n named: /tag manager|\\bgtm\\b/i,\n },\n {\n arg: 'metaPixel',\n path: 'analytics.adTags.meta',\n label: 'the Meta pixel',\n pattern: META_PIXEL_ID_PATTERN,\n shape: '8–20 digits',\n field: 'Meta pixel ID',\n ask: 'What’s your Meta pixel ID? It’s the number (8–20 digits) shown for your pixel in Meta Events Manager.',\n named: /\\bmeta\\b|facebook|instagram/i,\n },\n {\n arg: 'googleAds',\n path: 'analytics.adTags.google-ads',\n label: 'the Google Ads tag',\n pattern: GOOGLE_ADS_ID_PATTERN,\n shape: 'AW-123456789',\n field: 'Google Ads conversion ID',\n ask: 'What’s your Google Ads conversion ID? It looks like AW-123456789, from Google Ads.',\n named: /google ads|adwords/i,\n },\n {\n arg: 'linkedinInsight',\n path: 'analytics.adTags.linkedin',\n label: 'the LinkedIn Insight Tag',\n pattern: LINKEDIN_PARTNER_ID_PATTERN,\n shape: '4–10 digits',\n field: 'LinkedIn partner ID',\n ask: 'What’s your LinkedIn partner ID? It’s the number (4–10 digits) in LinkedIn Campaign Manager.',\n named: /linkedin/i,\n },\n {\n arg: 'tiktokPixel',\n path: 'analytics.adTags.tiktok',\n label: 'the TikTok pixel',\n pattern: TIKTOK_PIXEL_ID_PATTERN,\n shape: '16–24 capital letters and digits',\n field: 'TikTok pixel code',\n ask: 'What’s your TikTok pixel code? It’s in TikTok Ads Manager → Events, like C4ABCDEFGH1234567890.',\n named: /tiktok/i,\n },\n {\n arg: 'pinterestTag',\n path: 'analytics.adTags.pinterest',\n label: 'the Pinterest tag',\n pattern: PINTEREST_TAG_ID_PATTERN,\n shape: '10–16 digits',\n field: 'Pinterest tag ID',\n ask: 'What’s your Pinterest tag ID? It’s the number (10–16 digits) in Pinterest Ads → Conversions.',\n named: /pinterest/i,\n },\n]\n\n/** Asked when an item names no tag at all. */\nexport const AI_TRACKING_WHICH_COPY =\n 'Which tag should be connected, and what is its ID? Google Analytics, Google Tag Manager, the Meta pixel, Google Ads, LinkedIn, TikTok or Pinterest.'\n\nconst TAG_ARGS = AI_TRACKING_TAGS.map((tag) => tag.arg)\n\nconst ID_RULE = 'Exactly as the person gave it; leave it out unless the request states the id — never make one up.'\n\n/** The item's arguments, as the planner fills them in. Every one optional. */\nexport const AI_TRACKING_ARGS_SCHEMA: PluginAiCapabilityArgsSchema = {\n type: 'object',\n properties: {\n googleAnalytics: { type: 'string', description: `The Google Analytics 4 measurement ID, like G-XXXXXXXXXX. ${ID_RULE}`, maxLength: 24 },\n googleTagManager: { type: 'string', description: `The Google Tag Manager container ID, like GTM-XXXXXXX. ${ID_RULE}`, maxLength: 24 },\n metaPixel: { type: 'string', description: `The Meta (Facebook) pixel ID, 8–20 digits. ${ID_RULE}`, maxLength: 24 },\n googleAds: { type: 'string', description: `The Google Ads conversion ID, like AW-123456789. ${ID_RULE}`, maxLength: 24 },\n linkedinInsight: { type: 'string', description: `The LinkedIn Insight Tag partner ID, 4–10 digits. ${ID_RULE}`, maxLength: 24 },\n tiktokPixel: { type: 'string', description: `The TikTok pixel code, like C4ABCDEFGH1234567890. ${ID_RULE}`, maxLength: 32 },\n pinterestTag: { type: 'string', description: `The Pinterest tag ID, 10–16 digits. ${ID_RULE}`, maxLength: 24 },\n remove: {\n type: 'array',\n description: 'The tags the person asked to take off the site.',\n items: { type: 'string', enum: TAG_ARGS },\n maxItems: AI_TRACKING_TAGS.length,\n },\n },\n additionalProperties: false,\n}\n\n/** What an item asks: ids to set by field, and fields to clear. */\nexport interface AiTrackingRequest {\n set: Record<string, string>\n remove: string[]\n}\n\nexport interface AiTrackingRead {\n request: AiTrackingRequest\n /** A sentence per value that could not be used, so the rest still are. */\n problems: string[]\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The item's arguments read: what may be written, and a sentence for each that may not. */\nexport function aiTrackingRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiTrackingRead {\n const set: Record<string, string> = {}\n const problems: string[] = []\n const removing = new Set(Array.isArray(inputs?.['remove']) ? (inputs?.['remove'] as unknown[]).map((one) => String(one)) : [])\n const remove: string[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = text(inputs?.[tag.arg])\n if (value && removing.has(tag.arg)) {\n problems.push(`${capitalized(tag.label)} was both set and removed, so it was left as it is.`)\n continue\n }\n if (removing.has(tag.arg)) {\n remove.push(tag.path)\n continue\n }\n if (!value) continue\n // The card's own test, and the loader's: an id of any other shape is\n // saved by nobody, since it would never load.\n if (tag.pattern.test(value)) set[tag.path] = value\n else problems.push(`“${value.slice(0, 40)}” is not an ID for ${tag.label}, which looks like ${tag.shape}.`)\n }\n return { request: { set, remove }, problems }\n}\n\nfunction capitalized(label: string): string {\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/**\n * The question a planned tracking change asks the person, or `null` when it\n * sets or removes something (AGL-3751). An item with no usable id — the\n * planner kept \"add my Meta pixel\" with no id, or an id of the wrong shape —\n * is never built: it asks for the id of the tag its arguments or its name\n * name, in the card's own words, and says why an id given was not one.\n */\nexport function aiTrackingMissingInput(item: AiTrackingItem): string | null {\n const read = aiTrackingRequestOf(item.args)\n if (!aiTrackingRequestEmpty(read.request)) return null\n const named = aiTrackingTagsAsked(item)\n const asks = named.length ? named.map((tag) => tag.ask) : [AI_TRACKING_WHICH_COPY]\n return [...read.problems, ...asks].join(' ')\n}\n\ntype AiTrackingItem = { name?: string; why?: string; args: Readonly<Record<string, unknown>> }\n\n/** The tags an item asks about: those its arguments tried to set, else the ones its name or reason names. */\nfunction aiTrackingTagsAsked(item: AiTrackingItem): AiTrackingTag[] {\n const tried = AI_TRACKING_TAGS.filter((tag) => text(item.args[tag.arg]))\n const naming = (words: string | undefined) => AI_TRACKING_TAGS.filter((tag) => tag.named.test(words ?? ''))\n const byName = naming(item.name)\n return tried.length ? tried : byName.length ? byName : naming(item.why)\n}\n\n/**\n * The fields a planned tracking change with no usable id waits for (AGL-3978):\n * one per tag it asks about, labeled with the provider's id and held to the\n * card's pattern, so the build pauses in `needs_input` for the person to give\n * it. Empty when the item can be built, or names no tag at all.\n */\nexport function aiTrackingInputFields(item: AiTrackingItem): PluginAiCapabilityInputField[] {\n if (!aiTrackingMissingInput(item)) return []\n return aiTrackingTagsAsked(item).map((tag) => ({ arg: tag.arg, label: tag.field, shape: tag.shape, pattern: tag.pattern }))\n}\n\n/** Whether a read request asks for anything at all. */\nexport function aiTrackingRequestEmpty(request: AiTrackingRequest): boolean {\n return Object.keys(request.set).length === 0 && request.remove.length === 0\n}\n\n// ── The write ─────────────────────────────────────────────────────────────\n\nconst getPath = (data: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((at, key) => (at && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined), data)\n\n/** One change to the host document, with the sentence that says it was made. */\nexport interface AiTrackingChange {\n path: string\n /** The id to store, or `null` to delete the field. */\n value: string | null\n said: string\n}\n\n/** Every change the request asks, in the card's order, before comparing with what is stored. */\nexport function aiTrackingChanges(request: AiTrackingRequest): AiTrackingChange[] {\n const changes: AiTrackingChange[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = request.set[tag.path]\n if (value) changes.push({ path: tag.path, value, said: `${capitalized(tag.label)} is connected (${value}).` })\n else if (request.remove.includes(tag.path)) changes.push({ path: tag.path, value: null, said: `${capitalized(tag.label)} is removed.` })\n }\n return changes\n}\n\n/**\n * What the person should know about the tags the site now carries, in the\n * card's own words: where the consent banner is off, nothing of ours gates\n * them; and a container beside a direct Google tag counts everything twice.\n */\nexport function aiTrackingWarnings(host: Record<string, unknown>): string[] {\n const warnings: string[] = []\n const has = (path: string) => {\n const value = getPath(host, path)\n return typeof value === 'string' && value !== ''\n }\n const any = AI_TRACKING_TAGS.some((tag) => has(tag.path))\n if (any && getPath(host, 'consent.disabled') === true) {\n warnings.push('This site’s cookie banner is turned off because it uses its own consent tool, so that tool decides when these load.')\n }\n if (has('analytics.gtmContainerId') && (has('analytics.gaMeasurementId') || has('analytics.adTags.google-ads'))) {\n warnings.push('Do not also put an Analytics or Google Ads tag in the Tag Manager container: that loads the same measurement twice and counts every visit and conversion twice.')\n }\n return warnings\n}\n\nexport type AiTrackingWrite =\n | { ok: true; done: string[]; changed: string[]; warnings: string[]; outboxId: string | null }\n | { ok: false; error: string }\n\n/**\n * The tags written, in one transaction that reads the host as it is stored\n * NOW: the member still holds a write role, and only the values that differ\n * from what is stored are written, with a site-wide publish outbox entry\n * beside them so the live site's cached pages are dropped.\n */\nexport async function writeAiSiteTracking(\n firestore: Firestore,\n input: { hostId: string; uid: string; request: AiTrackingRequest; now: Date },\n): Promise<AiTrackingWrite> {\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n return firestore.runTransaction(async (tx): Promise<AiTrackingWrite> => {\n const host = await tx.get(hostRef)\n if (!host.exists) return { ok: false, error: 'Unknown site' }\n const roles = (host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return { ok: false, error: AI_TRACKING_ROLE_COPY }\n const data = (host.data() ?? {}) as Record<string, unknown>\n const changes = aiTrackingChanges(input.request)\n const changed = changes.filter((change) => {\n const stored = getPath(data, change.path)\n return change.value === null ? stored != null && stored !== '' : stored !== change.value\n })\n // What the site carries once this lands, for the warnings.\n // Only the two maps the warnings read; both are plain strings and flags.\n const after: Record<string, unknown> = JSON.parse(JSON.stringify({ analytics: data['analytics'] ?? {}, consent: data['consent'] ?? {} }))\n for (const change of changed) {\n const keys = change.path.split('.')\n const leaf = keys.pop() as string\n let node = after as Record<string, unknown>\n for (const key of keys) {\n if (!node[key] || typeof node[key] !== 'object') node[key] = {}\n node = node[key] as Record<string, unknown>\n }\n if (change.value === null) delete node[leaf]\n else node[leaf] = change.value\n }\n const done = changes.map((change) => change.said)\n const warnings = aiTrackingWarnings(after)\n if (!changed.length) return { ok: true, done, changed: [], warnings, outboxId: null }\n const updates: Record<string, unknown> = { updatedAt: input.now }\n for (const change of changed) updates[change.path] = change.value === null ? FieldValue.delete() : change.value\n tx.update(hostRef, updates)\n // The tags are in every page's head, so every cached page is stale.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n tx.set(outboxRef, {\n hostId: input.hostId,\n paths: [SCREEN_ROOT_PATH],\n entireHost: true,\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n })\n return { ok: true, done, changed: changed.map((change) => change.path), warnings, outboxId: outboxRef.id }\n })\n}\n\n// ── The step ──────────────────────────────────────────────────────────────\n\n/** Why this member may not change this site's tracking now, or `null`. */\nexport async function aiTrackingAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n if (!input.host.exists) return 'Unknown site'\n const roles = (input.host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return AI_TRACKING_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_TRACKING_LOCKED_COPY : null\n}\n\n/** The output's note: what changed, that it is live, how consent gates it, and what was not done. */\nexport function aiTrackingOutputNote(done: readonly string[], warnings: readonly string[], notDone: readonly string[], changedAny: boolean): string {\n const did = done.length\n ? `${done.join(' ')} Live now, in ${AI_TRACKING_WHERE}.${changedAny ? '' : ' It already was, so nothing was written.'}`\n : 'Nothing needed changing.'\n // The card's own promise, true of a tag whoever saved it.\n const consent = done.length ? ' Tags load only where a visitor’s cookie consent allows them.' : ''\n const warn = warnings.length ? ` ${warnings.join(' ')}` : ''\n const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : ''\n return `${did}${consent}${warn}${not}`.slice(0, 1_000)\n}\n\nexport interface AiJobTrackingStepDeps {\n write?: typeof writeAiSiteTracking\n lockdown?: typeof getLockdownVerdict\n dropCache?: typeof dropPluginSiteCache\n}\n\nexport function createAiJobTrackingStep(deps: AiJobTrackingStepDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiSiteTracking\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n return async ({ job, now, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_TRACKING_NO_SITE_COPY)\n const read = aiTrackingRequestOf(job.inputs)\n if (aiTrackingRequestEmpty(read.request)) return unspent([AI_TRACKING_NOTHING_COPY, ...read.problems].join(' '))\n const orgData = (org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)) as Record<string, unknown> | null\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const written = await write(firestore, { hostId, uid: job.createdBy, request: read.request, now })\n if (written.ok === false) return unspent(written.error)\n if (written.outboxId) {\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({ hostIds: [hostId], reason: 'AI build changed site tracking' }).catch(() => ({ complete: false }))\n if (dropped.complete) await firestore.collection('publishOutbox').doc(written.outboxId).delete().catch(() => undefined)\n }\n const output: AiJobOutput = {\n resource: 'tracking',\n id: AI_TRACKING_OUTPUT_ID,\n hostId,\n hostSubdomain: (typeof host.get('subdomain') === 'string' && host.get('subdomain')) || null,\n label: 'Site tracking',\n note: aiTrackingOutputNote(written.done, written.warnings, read.problems, written.changed.length > 0),\n }\n return aiUnspentOutcome(model, { outputs: [output] })\n }\n}\n\nexport const runAiJobTrackingStep = createAiJobTrackingStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiTrackingJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A tracking change is admitted on a site of the job's own org, for a member\n * who may write its settings now, where the request names at least one tag\n * this can set or remove. Refused in those words before anything is written.\n */\nexport function createAiTrackingJobAdmission(deps: AiTrackingJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_TRACKING_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const read = aiTrackingRequestOf(context.inputs as Record<string, unknown>)\n return aiTrackingRequestEmpty(read.request) ? { status: 400, error: [AI_TRACKING_NOTHING_COPY, ...read.problems].join(' ') } : null\n }\n}\n\nexport const aiTrackingJobAdmission = createAiTrackingJobAdmission()\n\n/** Registers the tracking step and its door (AGL-3751). */\nexport function registerAiTrackingJob(): void {\n registerAiJobStep(AI_TRACKING_KIND, runAiJobTrackingStep, { minimumMs: AI_JOB_TRACKING_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_TRACKING_KIND, aiTrackingJobAdmission)\n}\n"],"names":["createResourceUid","hostRoleCanWrite","SCREEN_ROOT_PATH","GA_MEASUREMENT_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","GTM_CONTAINER_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","dropPluginSiteCache","getLockdownVerdict","resolveOrgIdForHost","FieldValue","registerAiJobAdmission","aiJobEditModel","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_TRACKING_KIND","AI_JOB_TRACKING_STEP_MINIMUM_MS","AI_TRACKING_NO_SITE_COPY","AI_TRACKING_ROLE_COPY","AI_TRACKING_LOCKED_COPY","AI_TRACKING_NOTHING_COPY","AI_TRACKING_WHERE","AI_TRACKING_OUTPUT_ID","AI_TRACKING_TAGS","arg","path","label","pattern","shape","field","ask","named","AI_TRACKING_WHICH_COPY","TAG_ARGS","map","tag","ID_RULE","AI_TRACKING_ARGS_SCHEMA","type","properties","googleAnalytics","description","maxLength","googleTagManager","metaPixel","googleAds","linkedinInsight","tiktokPixel","pinterestTag","remove","items","enum","maxItems","length","additionalProperties","text","value","trim","aiTrackingRequestOf","inputs","set","problems","removing","Set","Array","isArray","one","String","has","push","capitalized","test","slice","request","charAt","toUpperCase","aiTrackingMissingInput","item","read","args","aiTrackingRequestEmpty","aiTrackingTagsAsked","asks","join","tried","filter","naming","words","byName","name","why","aiTrackingInputFields","Object","keys","getPath","data","split","reduce","at","key","undefined","aiTrackingChanges","changes","said","includes","aiTrackingWarnings","host","warnings","any","some","writeAiSiteTracking","firestore","input","hostRef","collection","doc","hostId","runTransaction","tx","get","exists","ok","error","roles","uid","changed","change","stored","after","JSON","parse","stringify","analytics","consent","leaf","pop","node","done","outboxId","updates","updatedAt","now","delete","update","outboxRef","paths","entireHost","createdAt","serverTimestamp","attempts","id","aiTrackingAccessRefusal","deps","locked","lockdown","staff","org","intent","aiTrackingOutputNote","notDone","changedAny","did","warn","not","createAiJobTrackingStep","write","dropCache","job","modelFor","model","unspent","failure","orgData","orgId","refused","createdBy","review","written","dropped","hostIds","reason","catch","complete","output","resource","hostSubdomain","note","outputs","runAiJobTrackingStep","createAiTrackingJobAdmission","ownerOf","context","status","owner","aiTrackingJobAdmission","registerAiTrackingJob","minimumMs"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-tracking-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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { holdsSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport {\n GA_MEASUREMENT_ID_PATTERN,\n GOOGLE_ADS_ID_PATTERN,\n GTM_CONTAINER_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n PINTEREST_TAG_ID_PATTERN,\n TIKTOK_PIXEL_ID_PATTERN,\n} from '@aglyn/aglyn/app-utils/visitor-consent'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport type { PluginAiCapabilityArgsSchema, PluginAiCapabilityInputField } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldValue, type Firestore } from 'firebase-admin/firestore'\nimport type { AiJobOutput } from '../model/ai-jobs.types'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobEditModel } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `tracking` step (AGL-3751): the site's analytics and advertising tags,\n * as the console's Setup → Tracking card sets them — Google Analytics, Google\n * Tag Manager, and the Meta, Google Ads, LinkedIn, TikTok and Pinterest tags —\n * as a first-class build operation, so \"connect Google Analytics G-4R7KX2PQ9Z\"\n * is one item on Assist's plan card.\n *\n * NO MODEL. The planner already wrote the ids as the item's arguments; this\n * holds each to the pattern the card's field (and the tenant's loader) holds\n * it to and writes it, so it costs no credits.\n *\n * ── Only the card's own fields, and only public ids ──────────────────────\n *\n * Every field written is one the Tracking card writes, under the same name:\n * `analytics.gaMeasurementId`, `analytics.gtmContainerId` and\n * `analytics.adTags.{meta,google-ads,linkedin,tiktok,pinterest}`, each tested\n * by the same expression (`GA_MEASUREMENT_ID_PATTERN` and its siblings in\n * `visitor-consent.ts`). Each is an id that is printed in every page's HTML\n * anyway, never a credential. Removing one deletes the field, as the card's\n * cleared box does (`CLEARABLE_TRACKING_PATHS`).\n *\n * NEVER: the consent banner (`consent.*` — turning it off, its mode, or the\n * advertising question), search indexing, verification tokens, and anything\n * that needs a secret the person pastes — the Ad conversions card's access\n * tokens, Zapier, order webhooks' signing secrets. Those are not arguments.\n * The tags load only where the consent banner lets them, exactly as a tag the\n * card saved does: the tenant gates on the stored ids, not on who wrote them.\n *\n * ── Live on confirm, who may, idempotent ─────────────────────────────────\n *\n * As the `settings` step (AGL-3748): a tag has no draft, so the plan card's\n * confirm is the agreement and the write is live. One transaction re-reads\n * the host, re-checks the role, writes only what differs with a site-wide\n * publish outbox entry, and the site's cached pages are dropped after. The\n * gate is the Firestore rules' for the host document (`canWriteHostContent`,\n * `hostWritesFrozen`): an admin, editor or author, while no lockdown freezes\n * the site, its org or the platform.\n */\n\nexport const AI_TRACKING_KIND = 'tracking' as const\n\n/** ASSUMED: the least time the step needs — the site, the org and one transaction. */\nexport const AI_JOB_TRACKING_STEP_MINIMUM_MS = 5_000\n\nexport const AI_TRACKING_NO_SITE_COPY = 'Open the site whose tracking should change.'\n/** Names \"Edit site content\" (`site.edit`, AGL-4047), never a role. */\nexport const AI_TRACKING_ROLE_COPY = `Changing the site’s tracking needs the \"${orgPermissionLabel('site.edit')}\" permission on this site — ask an organization admin.`\nexport const AI_TRACKING_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_TRACKING_NOTHING_COPY = 'The request named no tracking id this could set or remove.'\n\n/** Where a person finds what the op changed: the card that edits it. */\nexport const AI_TRACKING_WHERE = 'Setup → Tracking'\n\n/** The output's fixed id: one site's tracking, whatever changed. */\nexport const AI_TRACKING_OUTPUT_ID = 'tracking'\n\n/** One tag the card sets: its argument, its stored field, its name, and its pattern. */\nexport interface AiTrackingTag {\n arg: string\n path: string\n label: string\n pattern: RegExp\n /** What the card's field says the id looks like. */\n shape: string\n /** The id's name, as a field asking for it is labeled (AGL-3978): it names the provider. */\n field: string\n /** Asked when a request names this tag and no usable id, from the card's helper text. */\n ask: string\n /** Words that name this tag in an item's name. */\n named: RegExp\n}\n\n/** The Tracking card's fields, in its order (`trackingSchema`, host-settings-scope.tsx). */\nexport const AI_TRACKING_TAGS: readonly AiTrackingTag[] = [\n {\n arg: 'googleAnalytics',\n path: 'analytics.gaMeasurementId',\n label: 'Google Analytics',\n pattern: GA_MEASUREMENT_ID_PATTERN,\n shape: 'G-XXXXXXXXXX',\n field: 'Google Analytics measurement ID',\n ask: 'What’s your Google Analytics measurement ID? It looks like G-XXXXXXXXXX.',\n named: /google analytics|\\bga4?\\b|\\banalytics\\b/i,\n },\n {\n arg: 'googleTagManager',\n path: 'analytics.gtmContainerId',\n label: 'Google Tag Manager',\n pattern: GTM_CONTAINER_ID_PATTERN,\n shape: 'GTM-XXXXXXX',\n field: 'Google Tag Manager container ID',\n ask: 'What’s your Google Tag Manager container ID? It looks like GTM-XXXXXXX.',\n named: /tag manager|\\bgtm\\b/i,\n },\n {\n arg: 'metaPixel',\n path: 'analytics.adTags.meta',\n label: 'the Meta pixel',\n pattern: META_PIXEL_ID_PATTERN,\n shape: '8–20 digits',\n field: 'Meta pixel ID',\n ask: 'What’s your Meta pixel ID? It’s the number (8–20 digits) shown for your pixel in Meta Events Manager.',\n named: /\\bmeta\\b|facebook|instagram/i,\n },\n {\n arg: 'googleAds',\n path: 'analytics.adTags.google-ads',\n label: 'the Google Ads tag',\n pattern: GOOGLE_ADS_ID_PATTERN,\n shape: 'AW-123456789',\n field: 'Google Ads conversion ID',\n ask: 'What’s your Google Ads conversion ID? It looks like AW-123456789, from Google Ads.',\n named: /google ads|adwords/i,\n },\n {\n arg: 'linkedinInsight',\n path: 'analytics.adTags.linkedin',\n label: 'the LinkedIn Insight Tag',\n pattern: LINKEDIN_PARTNER_ID_PATTERN,\n shape: '4–10 digits',\n field: 'LinkedIn partner ID',\n ask: 'What’s your LinkedIn partner ID? It’s the number (4–10 digits) in LinkedIn Campaign Manager.',\n named: /linkedin/i,\n },\n {\n arg: 'tiktokPixel',\n path: 'analytics.adTags.tiktok',\n label: 'the TikTok pixel',\n pattern: TIKTOK_PIXEL_ID_PATTERN,\n shape: '16–24 capital letters and digits',\n field: 'TikTok pixel code',\n ask: 'What’s your TikTok pixel code? It’s in TikTok Ads Manager → Events, like C4ABCDEFGH1234567890.',\n named: /tiktok/i,\n },\n {\n arg: 'pinterestTag',\n path: 'analytics.adTags.pinterest',\n label: 'the Pinterest tag',\n pattern: PINTEREST_TAG_ID_PATTERN,\n shape: '10–16 digits',\n field: 'Pinterest tag ID',\n ask: 'What’s your Pinterest tag ID? It’s the number (10–16 digits) in Pinterest Ads → Conversions.',\n named: /pinterest/i,\n },\n]\n\n/** Asked when an item names no tag at all. */\nexport const AI_TRACKING_WHICH_COPY =\n 'Which tag should be connected, and what is its ID? Google Analytics, Google Tag Manager, the Meta pixel, Google Ads, LinkedIn, TikTok or Pinterest.'\n\nconst TAG_ARGS = AI_TRACKING_TAGS.map((tag) => tag.arg)\n\nconst ID_RULE = 'Exactly as the person gave it; leave it out unless the request states the id — never make one up.'\n\n/** The item's arguments, as the planner fills them in. Every one optional. */\nexport const AI_TRACKING_ARGS_SCHEMA: PluginAiCapabilityArgsSchema = {\n type: 'object',\n properties: {\n googleAnalytics: { type: 'string', description: `The Google Analytics 4 measurement ID, like G-XXXXXXXXXX. ${ID_RULE}`, maxLength: 24 },\n googleTagManager: { type: 'string', description: `The Google Tag Manager container ID, like GTM-XXXXXXX. ${ID_RULE}`, maxLength: 24 },\n metaPixel: { type: 'string', description: `The Meta (Facebook) pixel ID, 8–20 digits. ${ID_RULE}`, maxLength: 24 },\n googleAds: { type: 'string', description: `The Google Ads conversion ID, like AW-123456789. ${ID_RULE}`, maxLength: 24 },\n linkedinInsight: { type: 'string', description: `The LinkedIn Insight Tag partner ID, 4–10 digits. ${ID_RULE}`, maxLength: 24 },\n tiktokPixel: { type: 'string', description: `The TikTok pixel code, like C4ABCDEFGH1234567890. ${ID_RULE}`, maxLength: 32 },\n pinterestTag: { type: 'string', description: `The Pinterest tag ID, 10–16 digits. ${ID_RULE}`, maxLength: 24 },\n remove: {\n type: 'array',\n description: 'The tags the person asked to take off the site.',\n items: { type: 'string', enum: TAG_ARGS },\n maxItems: AI_TRACKING_TAGS.length,\n },\n },\n additionalProperties: false,\n}\n\n/** What an item asks: ids to set by field, and fields to clear. */\nexport interface AiTrackingRequest {\n set: Record<string, string>\n remove: string[]\n}\n\nexport interface AiTrackingRead {\n request: AiTrackingRequest\n /** A sentence per value that could not be used, so the rest still are. */\n problems: string[]\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The item's arguments read: what may be written, and a sentence for each that may not. */\nexport function aiTrackingRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiTrackingRead {\n const set: Record<string, string> = {}\n const problems: string[] = []\n const removing = new Set(Array.isArray(inputs?.['remove']) ? (inputs?.['remove'] as unknown[]).map((one) => String(one)) : [])\n const remove: string[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = text(inputs?.[tag.arg])\n if (value && removing.has(tag.arg)) {\n problems.push(`${capitalized(tag.label)} was both set and removed, so it was left as it is.`)\n continue\n }\n if (removing.has(tag.arg)) {\n remove.push(tag.path)\n continue\n }\n if (!value) continue\n // The card's own test, and the loader's: an id of any other shape is\n // saved by nobody, since it would never load.\n if (tag.pattern.test(value)) set[tag.path] = value\n else problems.push(`“${value.slice(0, 40)}” is not an ID for ${tag.label}, which looks like ${tag.shape}.`)\n }\n return { request: { set, remove }, problems }\n}\n\nfunction capitalized(label: string): string {\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/**\n * The question a planned tracking change asks the person, or `null` when it\n * sets or removes something (AGL-3751). An item with no usable id — the\n * planner kept \"add my Meta pixel\" with no id, or an id of the wrong shape —\n * is never built: it asks for the id of the tag its arguments or its name\n * name, in the card's own words, and says why an id given was not one.\n */\nexport function aiTrackingMissingInput(item: AiTrackingItem): string | null {\n const read = aiTrackingRequestOf(item.args)\n if (!aiTrackingRequestEmpty(read.request)) return null\n const named = aiTrackingTagsAsked(item)\n const asks = named.length ? named.map((tag) => tag.ask) : [AI_TRACKING_WHICH_COPY]\n return [...read.problems, ...asks].join(' ')\n}\n\ntype AiTrackingItem = { name?: string; why?: string; args: Readonly<Record<string, unknown>> }\n\n/** The tags an item asks about: those its arguments tried to set, else the ones its name or reason names. */\nfunction aiTrackingTagsAsked(item: AiTrackingItem): AiTrackingTag[] {\n const tried = AI_TRACKING_TAGS.filter((tag) => text(item.args[tag.arg]))\n const naming = (words: string | undefined) => AI_TRACKING_TAGS.filter((tag) => tag.named.test(words ?? ''))\n const byName = naming(item.name)\n return tried.length ? tried : byName.length ? byName : naming(item.why)\n}\n\n/**\n * The fields a planned tracking change with no usable id waits for (AGL-3978):\n * one per tag it asks about, labeled with the provider's id and held to the\n * card's pattern, so the build pauses in `needs_input` for the person to give\n * it. Empty when the item can be built, or names no tag at all.\n */\nexport function aiTrackingInputFields(item: AiTrackingItem): PluginAiCapabilityInputField[] {\n if (!aiTrackingMissingInput(item)) return []\n return aiTrackingTagsAsked(item).map((tag) => ({ arg: tag.arg, label: tag.field, shape: tag.shape, pattern: tag.pattern }))\n}\n\n/** Whether a read request asks for anything at all. */\nexport function aiTrackingRequestEmpty(request: AiTrackingRequest): boolean {\n return Object.keys(request.set).length === 0 && request.remove.length === 0\n}\n\n// ── The write ─────────────────────────────────────────────────────────────\n\nconst getPath = (data: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((at, key) => (at && typeof at === 'object' ? (at as Record<string, unknown>)[key] : undefined), data)\n\n/** One change to the host document, with the sentence that says it was made. */\nexport interface AiTrackingChange {\n path: string\n /** The id to store, or `null` to delete the field. */\n value: string | null\n said: string\n}\n\n/** Every change the request asks, in the card's order, before comparing with what is stored. */\nexport function aiTrackingChanges(request: AiTrackingRequest): AiTrackingChange[] {\n const changes: AiTrackingChange[] = []\n for (const tag of AI_TRACKING_TAGS) {\n const value = request.set[tag.path]\n if (value) changes.push({ path: tag.path, value, said: `${capitalized(tag.label)} is connected (${value}).` })\n else if (request.remove.includes(tag.path)) changes.push({ path: tag.path, value: null, said: `${capitalized(tag.label)} is removed.` })\n }\n return changes\n}\n\n/**\n * What the person should know about the tags the site now carries, in the\n * card's own words: where the consent banner is off, nothing of ours gates\n * them; and a container beside a direct Google tag counts everything twice.\n */\nexport function aiTrackingWarnings(host: Record<string, unknown>): string[] {\n const warnings: string[] = []\n const has = (path: string) => {\n const value = getPath(host, path)\n return typeof value === 'string' && value !== ''\n }\n const any = AI_TRACKING_TAGS.some((tag) => has(tag.path))\n if (any && getPath(host, 'consent.disabled') === true) {\n warnings.push('This site’s cookie banner is turned off because it uses its own consent tool, so that tool decides when these load.')\n }\n if (has('analytics.gtmContainerId') && (has('analytics.gaMeasurementId') || has('analytics.adTags.google-ads'))) {\n warnings.push('Do not also put an Analytics or Google Ads tag in the Tag Manager container: that loads the same measurement twice and counts every visit and conversion twice.')\n }\n return warnings\n}\n\nexport type AiTrackingWrite =\n | { ok: true; done: string[]; changed: string[]; warnings: string[]; outboxId: string | null }\n | { ok: false; error: string }\n\n/**\n * The tags written, in one transaction that reads the host as it is stored\n * NOW: the member still holds a write role, and only the values that differ\n * from what is stored are written, with a site-wide publish outbox entry\n * beside them so the live site's cached pages are dropped.\n */\nexport async function writeAiSiteTracking(\n firestore: Firestore,\n input: { hostId: string; uid: string; request: AiTrackingRequest; now: Date },\n): Promise<AiTrackingWrite> {\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n return firestore.runTransaction(async (tx): Promise<AiTrackingWrite> => {\n const host = await tx.get(hostRef)\n if (!host.exists) return { ok: false, error: 'Unknown site' }\n if (!holdsSiteKey(host, input.uid, 'site.edit')) return { ok: false, error: AI_TRACKING_ROLE_COPY }\n const data = (host.data() ?? {}) as Record<string, unknown>\n const changes = aiTrackingChanges(input.request)\n const changed = changes.filter((change) => {\n const stored = getPath(data, change.path)\n return change.value === null ? stored != null && stored !== '' : stored !== change.value\n })\n // What the site carries once this lands, for the warnings.\n // Only the two maps the warnings read; both are plain strings and flags.\n const after: Record<string, unknown> = JSON.parse(JSON.stringify({ analytics: data['analytics'] ?? {}, consent: data['consent'] ?? {} }))\n for (const change of changed) {\n const keys = change.path.split('.')\n const leaf = keys.pop() as string\n let node = after as Record<string, unknown>\n for (const key of keys) {\n if (!node[key] || typeof node[key] !== 'object') node[key] = {}\n node = node[key] as Record<string, unknown>\n }\n if (change.value === null) delete node[leaf]\n else node[leaf] = change.value\n }\n const done = changes.map((change) => change.said)\n const warnings = aiTrackingWarnings(after)\n if (!changed.length) return { ok: true, done, changed: [], warnings, outboxId: null }\n const updates: Record<string, unknown> = { updatedAt: input.now }\n for (const change of changed) updates[change.path] = change.value === null ? FieldValue.delete() : change.value\n tx.update(hostRef, updates)\n // The tags are in every page's head, so every cached page is stale.\n const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid())\n tx.set(outboxRef, {\n hostId: input.hostId,\n paths: [SCREEN_ROOT_PATH],\n entireHost: true,\n createdAt: FieldValue.serverTimestamp(),\n attempts: 0,\n })\n return { ok: true, done, changed: changed.map((change) => change.path), warnings, outboxId: outboxRef.id }\n })\n}\n\n// ── The step ──────────────────────────────────────────────────────────────\n\n/** Why this member may not change this site's tracking now, or `null`. */\nexport async function aiTrackingAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n if (!input.host.exists) return 'Unknown site'\n if (!holdsSiteKey(input.host, input.uid, 'site.edit')) return AI_TRACKING_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_TRACKING_LOCKED_COPY : null\n}\n\n/** The output's note: what changed, that it is live, how consent gates it, and what was not done. */\nexport function aiTrackingOutputNote(done: readonly string[], warnings: readonly string[], notDone: readonly string[], changedAny: boolean): string {\n const did = done.length\n ? `${done.join(' ')} Live now, in ${AI_TRACKING_WHERE}.${changedAny ? '' : ' It already was, so nothing was written.'}`\n : 'Nothing needed changing.'\n // The card's own promise, true of a tag whoever saved it.\n const consent = done.length ? ' Tags load only where a visitor’s cookie consent allows them.' : ''\n const warn = warnings.length ? ` ${warnings.join(' ')}` : ''\n const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : ''\n return `${did}${consent}${warn}${not}`.slice(0, 1_000)\n}\n\nexport interface AiJobTrackingStepDeps {\n write?: typeof writeAiSiteTracking\n lockdown?: typeof getLockdownVerdict\n dropCache?: typeof dropPluginSiteCache\n}\n\nexport function createAiJobTrackingStep(deps: AiJobTrackingStepDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiSiteTracking\n const dropCache = deps.dropCache ?? dropPluginSiteCache\n return async ({ job, now, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_TRACKING_NO_SITE_COPY)\n const read = aiTrackingRequestOf(job.inputs)\n if (aiTrackingRequestEmpty(read.request)) return unspent([AI_TRACKING_NOTHING_COPY, ...read.problems].join(' '))\n const orgData = (org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)) as Record<string, unknown> | null\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const written = await write(firestore, { hostId, uid: job.createdBy, request: read.request, now })\n if (written.ok === false) return unspent(written.error)\n if (written.outboxId) {\n // The live site's cached pages go now; the outbox entry stays for the\n // drain unless the drop certainly happened.\n const dropped = await dropCache({ hostIds: [hostId], reason: 'AI build changed site tracking' }).catch(() => ({ complete: false }))\n if (dropped.complete) await firestore.collection('publishOutbox').doc(written.outboxId).delete().catch(() => undefined)\n }\n const output: AiJobOutput = {\n resource: 'tracking',\n id: AI_TRACKING_OUTPUT_ID,\n hostId,\n hostSubdomain: (typeof host.get('subdomain') === 'string' && host.get('subdomain')) || null,\n label: 'Site tracking',\n note: aiTrackingOutputNote(written.done, written.warnings, read.problems, written.changed.length > 0),\n }\n return aiUnspentOutcome(model, { outputs: [output] })\n }\n}\n\nexport const runAiJobTrackingStep = createAiJobTrackingStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiTrackingJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A tracking change is admitted on a site of the job's own org, for a member\n * who may write its settings now, where the request names at least one tag\n * this can set or remove. Refused in those words before anything is written.\n */\nexport function createAiTrackingJobAdmission(deps: AiTrackingJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_TRACKING_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiTrackingAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const read = aiTrackingRequestOf(context.inputs as Record<string, unknown>)\n return aiTrackingRequestEmpty(read.request) ? { status: 400, error: [AI_TRACKING_NOTHING_COPY, ...read.problems].join(' ') } : null\n }\n}\n\nexport const aiTrackingJobAdmission = createAiTrackingJobAdmission()\n\n/** Registers the tracking step and its door (AGL-3751). */\nexport function registerAiTrackingJob(): void {\n registerAiJobStep(AI_TRACKING_KIND, runAiJobTrackingStep, { minimumMs: AI_JOB_TRACKING_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_TRACKING_KIND, aiTrackingJobAdmission)\n}\n"],"names":["createResourceUid","holdsSiteKey","orgPermissionLabel","SCREEN_ROOT_PATH","GA_MEASUREMENT_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","GTM_CONTAINER_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","dropPluginSiteCache","getLockdownVerdict","resolveOrgIdForHost","FieldValue","registerAiJobAdmission","aiJobEditModel","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_TRACKING_KIND","AI_JOB_TRACKING_STEP_MINIMUM_MS","AI_TRACKING_NO_SITE_COPY","AI_TRACKING_ROLE_COPY","AI_TRACKING_LOCKED_COPY","AI_TRACKING_NOTHING_COPY","AI_TRACKING_WHERE","AI_TRACKING_OUTPUT_ID","AI_TRACKING_TAGS","arg","path","label","pattern","shape","field","ask","named","AI_TRACKING_WHICH_COPY","TAG_ARGS","map","tag","ID_RULE","AI_TRACKING_ARGS_SCHEMA","type","properties","googleAnalytics","description","maxLength","googleTagManager","metaPixel","googleAds","linkedinInsight","tiktokPixel","pinterestTag","remove","items","enum","maxItems","length","additionalProperties","text","value","trim","aiTrackingRequestOf","inputs","set","problems","removing","Set","Array","isArray","one","String","has","push","capitalized","test","slice","request","charAt","toUpperCase","aiTrackingMissingInput","item","read","args","aiTrackingRequestEmpty","aiTrackingTagsAsked","asks","join","tried","filter","naming","words","byName","name","why","aiTrackingInputFields","Object","keys","getPath","data","split","reduce","at","key","undefined","aiTrackingChanges","changes","said","includes","aiTrackingWarnings","host","warnings","any","some","writeAiSiteTracking","firestore","input","hostRef","collection","doc","hostId","runTransaction","tx","get","exists","ok","error","uid","changed","change","stored","after","JSON","parse","stringify","analytics","consent","leaf","pop","node","done","outboxId","updates","updatedAt","now","delete","update","outboxRef","paths","entireHost","createdAt","serverTimestamp","attempts","id","aiTrackingAccessRefusal","deps","locked","lockdown","staff","org","intent","aiTrackingOutputNote","notDone","changedAny","did","warn","not","createAiJobTrackingStep","write","dropCache","job","modelFor","model","unspent","failure","orgData","orgId","refused","createdBy","review","written","dropped","hostIds","reason","catch","complete","output","resource","hostSubdomain","note","outputs","runAiJobTrackingStep","createAiTrackingJobAdmission","ownerOf","context","status","owner","aiTrackingJobAdmission","registerAiTrackingJob","minimumMs"],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type SweepAiJobsResult } from './ai-jobs';
18
18
  import { type AiEmptySiteSweepResult } from './ai-empty-site-sweep';
19
+ import { type AiPlanReminderSweepResult } from './ai-jobs-notify';
19
20
  /**
20
21
  * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
21
22
  *
@@ -71,16 +72,19 @@ export type AiJobsBeatResult = {
71
72
  } | ({
72
73
  held: false;
73
74
  emptySites: AiEmptySiteSweepResult | null;
75
+ planReminders: AiPlanReminderSweepResult | null;
74
76
  } & SweepAiJobsResult);
75
77
  /**
76
78
  * One beat: the switch, then the empty sites a guided start left that are
77
- * due for removal (AGL-3936), then one sweep under its own lease owner. The
78
- * owner is fresh per beat and fixed for the whole sweep, so two beats
79
- * overlapping inside a lease never read each other's lease as their own.
79
+ * due for removal (AGL-3936), then the one reminder a plan waiting a day
80
+ * for its confirmation is owed (AGL-4076), then one sweep under its own
81
+ * lease owner. The owner is fresh per beat and fixed for the whole sweep, so
82
+ * two beats overlapping inside a lease never read each other's lease as
83
+ * their own.
80
84
  *
81
- * The removals come first, a few a beat inside
82
- * `AI_EMPTY_SITE_SWEEP_BUDGET_MS`, and whatever they took comes off the
83
- * sweep's budget, so the route's arithmetic inside `maxDuration` holds. A
84
- * removal that fails never holds up a job's step.
85
+ * The removals and the reminders come first, each a few a beat inside its
86
+ * own budget, and whatever they took comes off the sweep's budget, so the
87
+ * route's arithmetic inside `maxDuration` holds. A removal or a reminder that
88
+ * fails never holds up a job's step.
85
89
  */
86
90
  export declare function runAiJobsBeat(): Promise<AiJobsBeatResult>;
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { featureLockdownRefusal, firebaseAdmin } from "@aglyn/tenant-data-admin";
19
19
  import { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs } from "./ai-jobs.js";
20
20
  import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
21
+ import { sweepAiPlanReminders } from "./ai-jobs-notify.js";
21
22
  /**
22
23
  * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
23
24
  *
@@ -66,14 +67,16 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
66
67
  */ export const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat';
67
68
  /**
68
69
  * One beat: the switch, then the empty sites a guided start left that are
69
- * due for removal (AGL-3936), then one sweep under its own lease owner. The
70
- * owner is fresh per beat and fixed for the whole sweep, so two beats
71
- * overlapping inside a lease never read each other's lease as their own.
70
+ * due for removal (AGL-3936), then the one reminder a plan waiting a day
71
+ * for its confirmation is owed (AGL-4076), then one sweep under its own
72
+ * lease owner. The owner is fresh per beat and fixed for the whole sweep, so
73
+ * two beats overlapping inside a lease never read each other's lease as
74
+ * their own.
72
75
  *
73
- * The removals come first, a few a beat inside
74
- * `AI_EMPTY_SITE_SWEEP_BUDGET_MS`, and whatever they took comes off the
75
- * sweep's budget, so the route's arithmetic inside `maxDuration` holds. A
76
- * removal that fails never holds up a job's step.
76
+ * The removals and the reminders come first, each a few a beat inside its
77
+ * own budget, and whatever they took comes off the sweep's budget, so the
78
+ * route's arithmetic inside `maxDuration` holds. A removal or a reminder that
79
+ * fails never holds up a job's step.
77
80
  */ export async function runAiJobsBeat() {
78
81
  const locked = await featureLockdownRefusal({
79
82
  feature: 'ai-generate'
@@ -93,6 +96,12 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
93
96
  return null;
94
97
  });
95
98
  if (emptySites == null ? void 0 : emptySites.removed) console.info(`ai jobs: ${emptySites.removed} empty site(s) removed`);
99
+ const planReminders = await sweepAiPlanReminders({
100
+ firestore
101
+ }).catch((error)=>{
102
+ console.error('ai jobs beat: the plan reminders failed', error);
103
+ return null;
104
+ });
96
105
  const result = await sweepAiJobs({
97
106
  firestore,
98
107
  owner: `beat:${randomUUID()}`,
@@ -103,7 +112,8 @@ import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
103
112
  }
104
113
  return _extends({
105
114
  held: false,
106
- emptySites
115
+ emptySites,
116
+ planReminders
107
117
  }, result);
108
118
  }
109
119