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

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 (177) hide show
  1. package/README.md +2 -2
  2. package/package.json +17 -17
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +8 -1
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/components/ai-assist-provider.component.js +2 -2
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  12. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  14. package/src/lib/components/ai-job-plan.component.js +21 -4
  15. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  16. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  17. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  18. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  19. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  20. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  21. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  22. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  23. package/src/lib/components/ai-product-zones.d.ts +124 -0
  24. package/src/lib/components/ai-product-zones.js +32 -0
  25. package/src/lib/components/ai-product-zones.js.map +1 -0
  26. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  27. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  28. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  29. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  30. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  31. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  32. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  35. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.js +2 -2
  37. package/src/lib/components/assist-panel.component.js.map +1 -1
  38. package/src/lib/components/assist-signals-page.component.js +2 -2
  39. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  40. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  41. package/src/lib/jobs/ai-email-bindings.js +75 -42
  42. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  43. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  44. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  45. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-component-step.js +9 -3
  47. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-drafts.js +1 -1
  49. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  50. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  51. package/src/lib/jobs/ai-job-email-step.js +1 -3
  52. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  53. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  56. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  57. package/src/lib/jobs/ai-job-generation.js +10 -2
  58. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  59. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  61. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  62. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  63. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  64. package/src/lib/jobs/ai-job-page-step.js +7 -2
  65. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  66. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  67. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  68. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  69. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  70. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  71. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  72. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  73. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  74. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  75. package/src/lib/jobs/ai-job-products-step.js +54 -32
  76. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  77. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  78. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  79. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  80. package/src/lib/jobs/ai-job-template-step.js +1 -0
  81. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  82. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  85. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  86. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  87. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  88. package/src/lib/jobs/ai-workflow-records.js +60 -27
  89. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  90. package/src/lib/model/ai-allotments.d.ts +11 -3
  91. package/src/lib/model/ai-allotments.js +21 -4
  92. package/src/lib/model/ai-allotments.js.map +1 -1
  93. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  94. package/src/lib/model/ai-automation-outline.js.map +1 -1
  95. package/src/lib/model/ai-build-plan.d.ts +57 -0
  96. package/src/lib/model/ai-build-plan.js +116 -7
  97. package/src/lib/model/ai-build-plan.js.map +1 -1
  98. package/src/lib/model/ai-seo.d.ts +11 -51
  99. package/src/lib/model/ai-seo.js +0 -17
  100. package/src/lib/model/ai-seo.js.map +1 -1
  101. package/src/lib/model/ai-site-inventory.js +1 -1
  102. package/src/lib/model/ai-site-inventory.js.map +1 -1
  103. package/src/lib/model/ai-template-subjects.js +2 -2
  104. package/src/lib/model/ai-template-subjects.js.map +1 -1
  105. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  106. package/src/lib/model/ai-theme-proposal.js +1 -1
  107. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  108. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  109. package/src/lib/model/ai-workflow-job.js +1 -2
  110. package/src/lib/model/ai-workflow-job.js.map +1 -1
  111. package/src/lib/plan-entitlements.d.ts +149 -0
  112. package/src/lib/plan-entitlements.js +168 -0
  113. package/src/lib/plan-entitlements.js.map +1 -0
  114. package/src/lib/plugin.js +1 -1
  115. package/src/lib/plugin.js.map +1 -1
  116. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  117. package/src/lib/runtime/ai-doctrine-validators.js +178 -21
  118. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  119. package/src/lib/runtime/ai-doctrine.js +5 -5
  120. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  121. package/src/lib/runtime/ai-eval-live.js +29 -14
  122. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  123. package/src/lib/runtime/ai-eval.d.ts +8 -1
  124. package/src/lib/runtime/ai-eval.js +29 -2
  125. package/src/lib/runtime/ai-eval.js.map +1 -1
  126. package/src/lib/runtime/ai-node-tree.js +3 -3
  127. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  128. package/src/lib/runtime/ai-palette.d.ts +6 -0
  129. package/src/lib/runtime/ai-palette.generated.js +15 -12
  130. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  131. package/src/lib/runtime/ai-palette.js +7 -1
  132. package/src/lib/runtime/ai-palette.js.map +1 -1
  133. package/src/lib/runtime/seo-audit.d.ts +13 -63
  134. package/src/lib/runtime/seo-audit.js +22 -207
  135. package/src/lib/runtime/seo-audit.js.map +1 -1
  136. package/src/lib/runtime/site-inventory.js +1 -1
  137. package/src/lib/runtime/site-inventory.js.map +1 -1
  138. package/src/lib/server/ai-assist-prompts.js +3 -3
  139. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  140. package/src/lib/server/assist-chat.js +11 -10
  141. package/src/lib/server/assist-chat.js.map +1 -1
  142. package/src/lib/server/assist-view-context.js +23 -23
  143. package/src/lib/server/assist-view-context.js.map +1 -1
  144. package/src/lib/server.d.ts +1 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  147. package/src/lib/testing/stand-in-automation-index.js +102 -0
  148. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  149. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  150. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  151. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  152. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  153. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  154. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  155. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  156. package/src/lib/testing/stand-in-product-index.js +105 -0
  157. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  158. package/src/lib/tools/ai-component-tool.js +2 -2
  159. package/src/lib/tools/ai-component-tool.js.map +1 -1
  160. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  161. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  162. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  163. package/src/lib/tools/ai-seo-tool.js +7 -38
  164. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  165. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  166. package/src/lib/tools/ai-workflow-tool.js +1 -1
  167. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  168. package/src/lib/usage/ai-allotment-alerts.js +8 -1
  169. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  170. package/src/lib/usage/assist-free-taste.js +2 -1
  171. package/src/lib/usage/assist-free-taste.js.map +1 -1
  172. package/src/lib/usage-axes.d.ts +39 -0
  173. package/src/lib/usage-axes.js +70 -0
  174. package/src/lib/usage-axes.js.map +1 -0
  175. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  176. package/src/lib/runtime/seo-page-facts.js +0 -150
  177. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -15,6 +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 { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
18
19
  import { isHostPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
19
20
  import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
20
21
  import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
@@ -76,7 +77,6 @@ export const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for t
76
77
  unreadable: 'The photo could not be read, so the copy is written from the product’s words.',
77
78
  model: 'The photo was not read: the model this job ran on does not read pictures.'
78
79
  };
79
- /** The commerce plugin, as `plugins.config.json` names it. */ const COMMERCE_PLUGIN_ID = 'commerce';
80
80
  /**
81
81
  * ASSUMED: how long a catalog or categories pass reads the site before it
82
82
  * asks: the host document and, for categories, the category names.
@@ -135,40 +135,54 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
135
135
  name: str(seo['title']) || str(host['displayName']) || subdomain || ''
136
136
  };
137
137
  }
138
- /** The site's categories by name, as many as a request lists. */ async function readCategories(firestore, hostId) {
139
- const snapshot = await firestore.collection('hosts').doc(hostId).collection('productCategories').select('name').limit(AI_PRODUCT_CATEGORY_LIST_MAX).get();
140
- return snapshot.docs.map((doc)=>({
141
- id: doc.id,
142
- name: str(doc.get('name'))
143
- })).filter((category)=>category.name).sort((a, b)=>a.name.localeCompare(b.name));
138
+ /**
139
+ * The site's categories by name, as many as a request lists — from the
140
+ * `productCategory` index the plugin that keeps them publishes (AGL-3080).
141
+ * None when no plugin keeps categories here.
142
+ */ async function readCategories(hostId, given) {
143
+ var _pluginRecordIndex;
144
+ const index = given != null ? given : (_pluginRecordIndex = pluginRecordIndex('productCategory')) == null ? void 0 : _pluginRecordIndex.index;
145
+ if (!index) return [];
146
+ const { records } = await index.list({
147
+ hostId,
148
+ limit: AI_PRODUCT_CATEGORY_LIST_MAX
149
+ });
150
+ return records.map((record)=>({
151
+ id: record.id,
152
+ name: record.name
153
+ })).sort((a, b)=>a.name.localeCompare(b.name));
144
154
  }
145
- /** A saved product's facts, as a bulk pass reads them; `null` when it is gone. */ async function readProductFacts(firestore, hostId, productId) {
146
- var _data_seo;
147
- const snapshot = await firestore.collection('hosts').doc(hostId).collection('products').doc(productId).get();
148
- const data = snapshot.exists ? snapshot.data() : null;
149
- if (!data || data['deletedAt']) return null;
150
- const name = str(data['name']);
151
- if (!name) return null;
152
- const type = data['type'];
153
- const seo = (_data_seo = data['seo']) != null ? _data_seo : {};
154
- const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : [];
155
+ /**
156
+ * A saved product's facts, as a bulk pass reads them; `null` when it is gone
157
+ * — read through the `product` index the plugin that keeps products publishes
158
+ * (AGL-3080), which answers live, named products only.
159
+ */ async function readProductFacts(hostId, productId) {
160
+ var _pluginRecordIndex;
161
+ const record = await ((_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.index.get({
162
+ hostId,
163
+ id: productId
164
+ }));
165
+ if (!record) return null;
166
+ const facts = record.facts;
167
+ const type = facts['type'];
168
+ const list = (value)=>Array.isArray(value) ? value : [];
155
169
  return {
156
- id: productId,
157
- name,
170
+ id: record.id,
171
+ name: record.name,
158
172
  type: AI_PRODUCT_TYPES.includes(type) ? type : 'physical',
159
- text: str(data['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),
160
- tags: (Array.isArray(data['tags']) ? data['tags'] : []).map(str).filter(Boolean),
161
- categoryIds: (Array.isArray(data['categoryIds']) ? data['categoryIds'] : []).map(str).filter(Boolean),
162
- options: (Array.isArray(data['options']) ? data['options'] : []).slice(0, AI_PRODUCT_OPTIONS_MAX).map((option)=>{
173
+ text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),
174
+ tags: list(facts['tags']).map(str).filter(Boolean),
175
+ categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),
176
+ options: list(facts['options']).slice(0, AI_PRODUCT_OPTIONS_MAX).map((option)=>{
163
177
  const record = option != null ? option : {};
164
178
  return {
165
179
  name: str(record['name']),
166
- values: (Array.isArray(record['values']) ? record['values'] : []).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)
180
+ values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)
167
181
  };
168
182
  }).filter((option)=>option.name),
169
- imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,
170
- seoTitle: str(seo['title']),
171
- seoDescription: str(seo['description'])
183
+ imageUrl: str(facts['imageUrl']) || null,
184
+ seoTitle: str(facts['seoTitle']),
185
+ seoDescription: str(facts['seoDescription'])
172
186
  };
173
187
  }
174
188
  function modelOf(context) {
@@ -188,7 +202,7 @@ function modelOf(context) {
188
202
  /** Ask for one product's copy, and build its proposal from the answer. */ async function writeProductCopy(context, model, site, product, deps) {
189
203
  var _ref;
190
204
  const { job, firestore, signal } = context;
191
- const categories = await readCategories(firestore, site.hostId);
205
+ const categories = await readCategories(site.hostId, deps.categoryIndex);
192
206
  let photo = 'none';
193
207
  let image = null;
194
208
  if (product.imageUrl) {
@@ -284,7 +298,7 @@ function modelOf(context) {
284
298
  const productId = inputs.productIds[done];
285
299
  if (!productId) return aiUnspentOutcome(model);
286
300
  const more = done + 1 < inputs.productIds.length;
287
- const product = await readProductFacts(firestore, site.hostId, productId);
301
+ const product = await readProductFacts(site.hostId, productId);
288
302
  if (!product) {
289
303
  const skipped = {
290
304
  kind: 'copy',
@@ -350,7 +364,7 @@ function modelOf(context) {
350
364
  ]
351
365
  });
352
366
  }
353
- const existing = await readCategories(firestore, site.hostId);
367
+ const existing = await readCategories(site.hostId, deps.categoryIndex);
354
368
  const generation = await generateAiCategories(_extends({
355
369
  store: site.name,
356
370
  brief: job.brief,
@@ -387,6 +401,7 @@ export const runAiJobProductsStep = createAiJobProductsStep();
387
401
  * refusal spends nothing.
388
402
  */ export const aiProductsJobAdmission = async (context)=>{
389
403
  var _data;
404
+ var _pluginRecordIndex;
390
405
  const inputs = parseAiProductsJobInputs(context.inputs);
391
406
  if (typeof inputs === 'string') return {
392
407
  status: 400,
@@ -406,14 +421,21 @@ export const runAiJobProductsStep = createAiJobProductsStep();
406
421
  status: 403,
407
422
  error: AI_PRODUCTS_NOT_ENTITLED_COPY
408
423
  };
424
+ // The plugin that keeps products is whichever publishes their index
425
+ // (AGL-3080); none is the same refusal as one switched off.
426
+ const keeper = (_pluginRecordIndex = pluginRecordIndex('product')) == null ? void 0 : _pluginRecordIndex.pluginId;
427
+ if (!keeper) return {
428
+ status: 403,
429
+ error: AI_PRODUCTS_COMMERCE_OFF_COPY
430
+ };
409
431
  const host = (_data = (await context.firestore.collection('hosts').doc(context.hostId).get()).data()) != null ? _data : null;
410
432
  const released = await filterEnabledPluginsByReleaseFlags([
411
- COMMERCE_PLUGIN_ID
433
+ keeper
412
434
  ], {
413
435
  orgId: context.orgId,
414
436
  authorization: null
415
437
  });
416
- if (!released.includes(COMMERCE_PLUGIN_ID) || !isHostPluginEnabled(org, host, COMMERCE_PLUGIN_ID)) {
438
+ if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {
417
439
  return {
418
440
  status: 403,
419
441
  error: AI_PRODUCTS_COMMERCE_OFF_COPY
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/** The commerce plugin, as `plugins.config.json` names it. */\nconst COMMERCE_PLUGIN_ID = 'commerce'\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/** The site's categories by name, as many as a request lists. */\nasync function readCategories(firestore: Firestore, hostId: string): Promise<AiProductCategory[]> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('productCategories')\n .select('name')\n .limit(AI_PRODUCT_CATEGORY_LIST_MAX)\n .get()\n return snapshot.docs\n .map((doc) => ({ id: doc.id, name: str(doc.get('name')) }))\n .filter((category) => category.name)\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/** A saved product's facts, as a bulk pass reads them; `null` when it is gone. */\nasync function readProductFacts(firestore: Firestore, hostId: string, productId: string): Promise<AiProductFacts | null> {\n const snapshot = await firestore.collection('hosts').doc(hostId).collection('products').doc(productId).get()\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n if (!data || data['deletedAt']) return null\n const name = str(data['name'])\n if (!name) return null\n const type = data['type']\n const seo = (data['seo'] ?? {}) as Record<string, unknown>\n const mediaUrls = Array.isArray(data['mediaUrls']) ? data['mediaUrls'] : []\n return {\n id: productId,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(data['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: (Array.isArray(data['tags']) ? data['tags'] : []).map(str).filter(Boolean),\n categoryIds: (Array.isArray(data['categoryIds']) ? data['categoryIds'] : []).map(str).filter(Boolean),\n options: (Array.isArray(data['options']) ? data['options'] : [])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: (Array.isArray(record['values']) ? record['values'] : [])\n .map(str)\n .filter(Boolean)\n .slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(mediaUrls[0]) || str(data['imageUrl']) || null,\n seoTitle: str(seo['title']),\n seoDescription: str(seo['description']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(firestore, site.hostId)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(firestore, site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(firestore, site.hostId)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([COMMERCE_PLUGIN_ID], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(COMMERCE_PLUGIN_ID) || !isHostPluginEnabled(org, host, COMMERCE_PLUGIN_ID)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","COMMERCE_PLUGIN_ID","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","select","limit","docs","map","id","filter","category","sort","a","b","localeCompare","readProductFacts","productId","type","mediaUrls","Array","isArray","includes","text","slice","tags","Boolean","categoryIds","options","option","record","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","photo","image","readsImages","read","status","generation","store","names","Map","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-products-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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n pluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { isHostPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n AI_PRODUCT_CATEGORY_LIST_MAX,\n AI_PRODUCT_OPTIONS_MAX,\n AI_PRODUCT_OPTION_VALUES_MAX,\n AI_PRODUCT_TEXT_MAX_CHARS,\n AI_PRODUCT_TYPES,\n AI_PRODUCTS_BULK_MAX,\n AI_PRODUCTS_OUTPUT_IDS,\n aiProductGaps,\n aiProductsProposalOf,\n parseAiProductsJobInputs,\n type AiCatalogProposal,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductCopyProposal,\n type AiProductFacts,\n type AiProductPhotoRead,\n type AiProductType,\n} from '../model/ai-products'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport type { AiValidatedGeneration } from '../runtime/ai-doctrine'\nimport {\n AI_CATALOG_MAX_TOKENS,\n AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n AI_PRODUCT_COPY_MAX_TOKENS,\n AI_PRODUCTS_STEP,\n generateAiCatalog,\n generateAiCategories,\n generateAiProductCopy,\n} from '../runtime/ai-products-generation'\nimport { aiRouteReadsImages } from '../runtime/ai-runtime'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport { AI_PRODUCT_IMAGE_READ_MS, readAiProductImage, type AiProductImageSeams } from './ai-product-image'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep, registerAiJobStepPasses } from './ai-jobs'\n\n/**\n * The `products` step (AGL-2916): commerce by AI, as proposals.\n *\n * `inputs.target` names the work (`model/ai-products.ts`): one product's copy\n * from what its editor holds, the copy of many saved products one a pass, a\n * store's first products from a brief, or its categories and a first set of\n * discounts from a brief.\n *\n * ## It writes nothing\n *\n * Like every runner, this reads, asks and returns. Every output is a\n * `product` proposal, and the commerce plugin's own surfaces write what a\n * person accepts: the product editor's Save, the products card's apply, and\n * the create paths the catalog and discount cards already use — each with the\n * allowance, role and seed checks those paths carry. A proposed product is\n * created as a draft with no price, and a proposed discount is created\n * switched off.\n *\n * ## A bulk job, a product at a time\n *\n * A bulk job's pass writes one product's copy and asks the machine to\n * continue while products remain, so each product is one reservation and one\n * provider exchange, and a job that runs out of credits resumes at the\n * product where it stopped. Which product is next is read from the job's own\n * outputs, one a product, so nothing is kept anywhere else. A product that is\n * gone, or whose copy cannot be held to the rules, is reported and passed\n * over rather than failing the products after it.\n *\n * ## What the model is shown\n *\n * The product's name, type, text, tags, options and current search listing,\n * the names of the site's categories, the store's name, and — for a model that\n * reads pictures — the product's first photo, read from the site's own media\n * library as `ai-product-image.ts` describes. A catalog or categories request\n * carries the brief and the store's name, and the second its existing category\n * names. No price, stock, order, customer or other product is read for any of\n * them.\n */\n\nexport const AI_PRODUCTS_NO_SITE_COPY = 'Open the store this job is for before starting it.'\nexport const AI_PRODUCTS_UNKNOWN_SITE_COPY = 'This AI job names a site this workspace does not have.'\nexport const AI_PRODUCTS_NOT_ENTITLED_COPY = 'Your plan does not include selling products. See Billing.'\nexport const AI_PRODUCTS_COMMERCE_OFF_COPY = 'Turn on Commerce for this site before starting the job.'\nexport const AI_PRODUCTS_GONE_COPY = 'This product is no longer in the catalog.'\nexport const AI_PRODUCTS_DECLINED_COPY = 'The model declined to write copy for this product.'\n\n/** The notes a proposal carries about its product's photo. */\nexport const AI_PRODUCT_PHOTO_NOTES: Readonly<Partial<Record<AiProductPhotoRead, string>>> = {\n 'not-in-library':\n 'The photo was not read: it is not in this site’s media library. The copy is written from the product’s words.',\n unreadable: 'The photo could not be read, so the copy is written from the product’s words.',\n model: 'The photo was not read: the model this job ran on does not read pictures.',\n}\n\n/**\n * ASSUMED: how long a catalog or categories pass reads the site before it\n * asks: the host document and, for categories, the category names.\n */\nexport const AI_PRODUCTS_SITE_READ_MS = 1_000\n\nconst TIER = AI_STEP_TIERS[AI_PRODUCTS_STEP]\n\n/**\n * One product's copy (AGL-3035): its generation and re-ask at the copy's own\n * ceiling on the tier `job.products` is served from, with the photo read. The\n * least any pass of the step needs, and so what the step registers.\n */\nexport const AI_JOB_PRODUCT_COPY_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_COPY_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCT_IMAGE_READ_MS,\n})\n\n/** A catalog: the largest answer the step asks for, at the routing table's ceiling. */\nexport const AI_JOB_CATALOG_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_CATALOG_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** Categories and discounts. */\nexport const AI_JOB_CATEGORIES_BUDGET = aiJobStepBudget({\n tier: TIER,\n maxTokens: AI_PRODUCT_CATEGORIES_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_PRODUCTS_SITE_READ_MS,\n})\n\n/** What the `products` step registers: the least any of its passes needs. */\nexport const AI_JOB_PRODUCTS_STEP_BUDGET = AI_JOB_PRODUCT_COPY_BUDGET\n\n/** The least time the next pass of a job needs, by what the job asks for. */\nexport function aiProductsRunMinimumMs(job: Pick<AiJob, 'inputs'>): number {\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n if (inputs.target === 'catalog') return AI_JOB_CATALOG_BUDGET.minimumMs\n if (inputs.target === 'categories') return AI_JOB_CATEGORIES_BUDGET.minimumMs\n return AI_JOB_PRODUCT_COPY_BUDGET.minimumMs\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface StoreSite {\n hostId: string\n subdomain: string | null\n /** The store's name, as its visitors read it. */\n name: string\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** Through JSON, so no `undefined` reaches the job document's write. */\nconst plain = <T>(value: T): Record<string, unknown> =>\n JSON.parse(JSON.stringify(value)) as Record<string, unknown>\n\n/** The site a job names, read and checked against the job's org. */\nasync function loadStore(firestore: Firestore, job: AiJob): Promise<StoreSite | { failure: string }> {\n if (!job.hostId) return { failure: AI_PRODUCTS_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // The door checks the caller's membership of the org the job is metered\n // against; a site of another org is refused here, before anything of it\n // reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const seo = (host['seo'] ?? {}) as Record<string, unknown>\n const subdomain = str(host['subdomain']) || null\n return {\n hostId: job.hostId,\n subdomain,\n name: str(seo['title']) || str(host['displayName']) || subdomain || '',\n }\n}\n\n/**\n * The site's categories by name, as many as a request lists — from the\n * `productCategory` index the plugin that keeps them publishes (AGL-3080).\n * None when no plugin keeps categories here.\n */\nasync function readCategories(\n hostId: string,\n given?: PluginRecordIndex | null,\n): Promise<AiProductCategory[]> {\n const index = given ?? pluginRecordIndex('productCategory')?.index\n if (!index) return []\n const { records } = await index.list({ hostId, limit: AI_PRODUCT_CATEGORY_LIST_MAX })\n return records\n .map((record) => ({ id: record.id, name: record.name }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * A saved product's facts, as a bulk pass reads them; `null` when it is gone\n * — read through the `product` index the plugin that keeps products publishes\n * (AGL-3080), which answers live, named products only.\n */\nasync function readProductFacts(hostId: string, productId: string): Promise<AiProductFacts | null> {\n const record = await pluginRecordIndex('product')?.index.get({ hostId, id: productId })\n if (!record) return null\n const facts = record.facts\n const type = facts['type']\n const list = (value: unknown): unknown[] => (Array.isArray(value) ? value : [])\n return {\n id: record.id,\n name: record.name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: str(facts['description']).slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: list(facts['tags']).map(str).filter(Boolean),\n categoryIds: list(facts['categoryIds']).map(str).filter(Boolean),\n options: list(facts['options'])\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n .map((option) => {\n const record = (option ?? {}) as Record<string, unknown>\n return {\n name: str(record['name']),\n values: list(record['values']).map(str).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX),\n }\n })\n .filter((option) => option.name),\n imageUrl: str(facts['imageUrl']) || null,\n seoTitle: str(facts['seoTitle']),\n seoDescription: str(facts['seoDescription']),\n }\n}\n\nfunction modelOf(context: AiJobStepContext): string {\n return context.modelFor?.(AI_PRODUCTS_STEP) ?? aiModelForStep(AI_PRODUCTS_STEP)\n}\n\n/** An output addressed to the site, so its link opens the site's products. */\nfunction output(site: StoreSite, id: string, label: string, proposal: object): AiJobOutput {\n return {\n resource: 'product',\n id,\n hostId: site.hostId,\n hostSubdomain: site.subdomain,\n label,\n proposal: plain(proposal),\n }\n}\n\nexport interface AiJobProductsStepDeps {\n image?: AiProductImageSeams\n /** Whether a model reads pictures; the provider's own descriptor otherwise. */\n readsImages?: (model: string) => boolean\n /**\n * Where the site's categories are read; the `productCategory` index the\n * plugin that keeps them publishes otherwise. The eval harness hands its\n * own, over the categories a case names.\n */\n categoryIndex?: PluginRecordIndex | null\n}\n\ninterface CopyRun {\n generation: AiValidatedGeneration<AiProductCopy>\n proposal: AiProductCopyProposal\n}\n\n/** Ask for one product's copy, and build its proposal from the answer. */\nasync function writeProductCopy(\n context: AiJobStepContext & { firestore: Firestore },\n model: string,\n site: StoreSite,\n product: AiProductFacts,\n deps: AiJobProductsStepDeps,\n): Promise<CopyRun> {\n const { job, firestore, signal } = context\n const categories = await readCategories(site.hostId, deps.categoryIndex)\n let photo: AiProductPhotoRead = 'none'\n let image = null\n if (product.imageUrl) {\n if ((deps.readsImages ?? aiRouteReadsImages)(model)) {\n const read = await readAiProductImage(\n firestore,\n { value: product.imageUrl, hostId: site.hostId, orgId: job.orgId },\n deps.image,\n )\n photo = read.status\n image = read.status === 'read' ? read.image : null\n } else {\n photo = 'model'\n }\n }\n const generation = await generateAiProductCopy({\n store: site.name,\n product,\n categories,\n photo: Boolean(image),\n image,\n model,\n maxTokens: AI_JOB_PRODUCT_COPY_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const values = generation.status === 'ok' ? generation.value : null\n const names = new Map(categories.map((category) => [category.id, category.name]))\n const note = AI_PRODUCT_PHOTO_NOTES[photo]\n return {\n generation,\n proposal: {\n kind: 'copy',\n product: { id: product.id, name: product.name },\n values,\n categories: (values?.categoryIds ?? []).map((id) => ({ id, name: names.get(id) ?? id })),\n optionNamesBefore: product.options.map((option) => option.name),\n gaps: values ? aiProductGaps(values.description) : [],\n photo,\n skipped: null,\n notes: note ? [note] : [],\n },\n }\n}\n\n/** The step, with its reads replaceable where a spec needs them. */\nexport function createAiJobProductsStep(deps: AiJobProductsStepDeps = {}): AiJobStepRunner {\n return async (context) => {\n const { job, firestore, signal } = context\n const model = modelOf(context)\n const inputs = parseAiProductsJobInputs(job.inputs)\n if (typeof inputs === 'string') return { ...aiUnspentOutcome(model), failure: inputs }\n const site = await loadStore(firestore, job)\n if ('failure' in site) return { ...aiUnspentOutcome(model), failure: site.failure }\n\n if (inputs.target === 'product') {\n const { generation, proposal } = await writeProductCopy(context, model, site, inputs.product, deps)\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(inputs.product.id), `Product copy · ${inputs.product.name}`, proposal)],\n }\n }\n\n if (inputs.target === 'bulk') {\n const done = (job.outputs ?? []).filter((entry) => aiProductsProposalOf(entry)?.kind === 'copy').length\n const productId = inputs.productIds[done]\n if (!productId) return aiUnspentOutcome(model)\n const more = done + 1 < inputs.productIds.length\n const product = await readProductFacts(site.hostId, productId)\n if (!product) {\n const skipped: AiProductCopyProposal = {\n kind: 'copy',\n product: { id: productId, name: productId },\n values: null,\n categories: [],\n optionNamesBefore: [],\n gaps: [],\n photo: 'none',\n skipped: AI_PRODUCTS_GONE_COPY,\n notes: [],\n }\n return {\n ...aiUnspentOutcome(model, {\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${productId}`, skipped)],\n }),\n continue: more,\n }\n }\n const { generation, proposal } = await writeProductCopy(context, model, site, product, deps)\n const spent = aiGenerationSpent(generation)\n // One product the rules could not hold, or the model declined, is\n // reported and passed over: its tokens are metered with the pass, and\n // the products after it still get their copy.\n const reported: AiProductCopyProposal =\n generation.status === 'ok'\n ? proposal\n : {\n ...proposal,\n skipped: generation.status === 'needs_input' ? generation.message : AI_PRODUCTS_DECLINED_COPY,\n }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.copy(productId), `Product copy · ${product.name}`, reported)],\n continue: more,\n }\n }\n\n if (inputs.target === 'catalog') {\n const generation = await generateAiCatalog({\n store: site.name,\n brief: job.brief,\n model,\n maxTokens: AI_JOB_CATALOG_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCatalogProposal = { kind: 'catalog', products: generation.value, notes: [] }\n return {\n ...spent,\n outputs: [\n output(\n site,\n AI_PRODUCTS_OUTPUT_IDS.catalog,\n `Proposed products · ${generation.value.length}`,\n proposal,\n ),\n ],\n }\n }\n\n const existing = await readCategories(site.hostId, deps.categoryIndex)\n const generation = await generateAiCategories({\n store: site.name,\n brief: job.brief,\n existingCategoryNames: existing.map((category) => category.name),\n model,\n maxTokens: AI_JOB_CATEGORIES_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const proposal: AiCategoriesProposal = { kind: 'categories', ...generation.value, notes: [] }\n return {\n ...spent,\n outputs: [output(site, AI_PRODUCTS_OUTPUT_IDS.categories, 'Proposed categories and discounts', proposal)],\n }\n }\n}\n\nexport const runAiJobProductsStep = createAiJobProductsStep()\n\n/**\n * Whether a `products` job may start: inputs it can run, a site of the job's\n * own org, a plan that sells products, and Commerce on for the site and past\n * its release flag. Asked by the create door before the job exists, so a\n * refusal spends nothing.\n */\nexport const aiProductsJobAdmission: AiJobAdmission = async (context) => {\n const inputs = parseAiProductsJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n if (!context.hostId) return { status: 400, error: AI_PRODUCTS_NO_SITE_COPY }\n const owner = await resolveOrgIdForHost(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: AI_PRODUCTS_UNKNOWN_SITE_COPY }\n const org = context.org as (Partial<AglynOrgBilling> & { enabledPlugins?: string[] }) | null\n if (!checkEntitlement(org, 'commerce')) return { status: 403, error: AI_PRODUCTS_NOT_ENTITLED_COPY }\n // The plugin that keeps products is whichever publishes their index\n // (AGL-3080); none is the same refusal as one switched off.\n const keeper = pluginRecordIndex('product')?.pluginId\n if (!keeper) return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n const host = (await context.firestore.collection('hosts').doc(context.hostId).get()).data() ?? null\n const released = await filterEnabledPluginsByReleaseFlags([keeper], {\n orgId: context.orgId,\n authorization: null,\n })\n if (!released.includes(keeper) || !isHostPluginEnabled(org, host, keeper)) {\n return { status: 403, error: AI_PRODUCTS_COMMERCE_OFF_COPY }\n }\n return null\n}\n\n/**\n * Registers the `products` step, the least time each of its passes needs, its\n * pass bound and its admission.\n */\nexport function registerAiProductsJob(): void {\n registerAiJobStep('products', runAiJobProductsStep, {\n minimumMs: AI_JOB_PRODUCTS_STEP_BUDGET.minimumMs,\n minimumMsFor: aiProductsRunMinimumMs,\n })\n registerAiJobStepPasses('products', AI_PRODUCTS_BULK_MAX)\n registerAiJobAdmission('products', aiProductsJobAdmission)\n}\n"],"names":["checkEntitlement","pluginRecordIndex","isHostPluginEnabled","resolveOrgIdForHost","filterEnabledPluginsByReleaseFlags","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_TYPES","AI_PRODUCTS_BULK_MAX","AI_PRODUCTS_OUTPUT_IDS","aiProductGaps","aiProductsProposalOf","parseAiProductsJobInputs","AI_STEP_TIERS","aiModelForStep","AI_CATALOG_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCTS_STEP","generateAiCatalog","generateAiCategories","generateAiProductCopy","aiRouteReadsImages","registerAiJobAdmission","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_PRODUCT_IMAGE_READ_MS","readAiProductImage","registerAiJobStep","registerAiJobStepPasses","AI_PRODUCTS_NO_SITE_COPY","AI_PRODUCTS_UNKNOWN_SITE_COPY","AI_PRODUCTS_NOT_ENTITLED_COPY","AI_PRODUCTS_COMMERCE_OFF_COPY","AI_PRODUCTS_GONE_COPY","AI_PRODUCTS_DECLINED_COPY","AI_PRODUCT_PHOTO_NOTES","unreadable","model","AI_PRODUCTS_SITE_READ_MS","TIER","AI_JOB_PRODUCT_COPY_BUDGET","tier","maxTokens","lookups","ownReadsMs","AI_JOB_CATALOG_BUDGET","AI_JOB_CATEGORIES_BUDGET","AI_JOB_PRODUCTS_STEP_BUDGET","aiProductsRunMinimumMs","job","inputs","minimumMs","target","str","value","trim","plain","JSON","parse","stringify","loadStore","firestore","host","hostId","failure","snapshot","collection","doc","get","exists","data","orgId","seo","subdomain","name","readCategories","given","index","records","list","limit","map","record","id","sort","a","b","localeCompare","readProductFacts","productId","facts","type","Array","isArray","includes","text","slice","tags","filter","Boolean","categoryIds","options","option","values","imageUrl","seoTitle","seoDescription","modelOf","context","modelFor","output","site","label","proposal","resource","hostSubdomain","writeProductCopy","product","deps","signal","categories","categoryIndex","photo","image","readsImages","read","status","generation","store","names","Map","category","note","kind","optionNamesBefore","gaps","description","skipped","notes","createAiJobProductsStep","spent","refused","message","outputs","copy","done","entry","length","productIds","more","continue","reported","brief","products","catalog","existing","existingCategoryNames","runAiJobProductsStep","aiProductsJobAdmission","error","owner","org","keeper","pluginId","released","authorization","registerAiProductsJob","minimumMsFor"],"mappings":"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"}
@@ -19,7 +19,6 @@ import { type SeoListingFieldKey } from '@aglyn/aglyn/app-utils/seo-listing-fiel
19
19
  import type { AiJobOutput } from '../model/ai-jobs.types';
20
20
  import { type AiSeoAuditReport, type AiSeoPageFix, type AiSeoSiteFormField } from '../model/ai-seo';
21
21
  import type { AiSystemBlock } from '../runtime/ai-runtime';
22
- import { type AiSeoAuditPage } from '../runtime/seo-audit';
23
22
  import { type AiSeoBatchAnswer, type AiSeoBatchPage } from '../tools/ai-seo-tool';
24
23
  import { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS } from './ai-job-seo-budget';
25
24
  import type { AiJobStepRunner } from './ai-job-text-step';
@@ -48,8 +47,9 @@ import type { AiJobStepRunner } from './ai-job-text-step';
48
47
  * ## An audit, a unit at a time
49
48
  *
50
49
  * The first pass reads the site — every published page's version and the
51
- * shared layouts, for the links between them — scores it without a model,
52
- * records the report, and runs the first unit of generated work. Each later
50
+ * shared layouts, for the links between them — and scores it without a
51
+ * model, through the platform's SEO check (the same findings the site's SEO
52
+ * section lists to every owner); it records the report, and runs the first unit of generated work. Each later
53
53
  * pass runs one more unit: the site-wide proposal, then the page fixes in
54
54
  * batches. Every pass is one reservation and one provider exchange, and it
55
55
  * asks the machine to continue until nothing is left, so a large site is a
@@ -59,8 +59,6 @@ export declare const AI_SEO_NO_SITE_COPY = "This SEO job names a site this works
59
59
  export declare const AI_SEO_NO_PAGE_COPY = "This page is no longer on the site.";
60
60
  export declare const AI_SEO_NO_TARGET_COPY = "This SEO job does not say what to write about.";
61
61
  export declare const AI_SEO_NO_PRODUCT_COPY = "Give the product a name before writing its listing.";
62
- /** Shared layouts an audit reads for the links in a site's navigation. */
63
- export declare const AI_SEO_LAYOUT_SCAN_LIMIT = 20;
64
62
  /** How much of each page a batch of fixes carries. */
65
63
  export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
66
64
  /**
@@ -70,7 +68,6 @@ export declare const AI_SEO_BATCH_PAGE_TEXT_CHARS = 700;
70
68
  * measured in `ai-job-seo-step.spec.ts`.
71
69
  */
72
70
  export { AI_SEO_FIXES_MAX_TOKENS, AI_SEO_SITE_MAX_TOKENS };
73
- type Firestore = FirebaseFirestore.Firestore;
74
71
  /** The host document fields this step reads. */
75
72
  export interface SeoHostDocument {
76
73
  orgId?: string;
@@ -103,24 +100,6 @@ export interface SeoHostDocument {
103
100
  * comma-separated) that the target's editor holds, or all of those.
104
101
  */
105
102
  export declare function aiSeoRequestedFields(raw: unknown, allowed: readonly SeoListingFieldKey[]): SeoListingFieldKey[];
106
- export interface AiSeoScannedPage extends AiSeoAuditPage {
107
- /** The page's node map as audited. */
108
- nodes: Record<string, unknown> | null;
109
- }
110
- export interface AiSeoSiteScan {
111
- pages: AiSeoScannedPage[];
112
- skipped: number;
113
- notes: string[];
114
- }
115
- /**
116
- * Read the site the way its sitemap lists it: every screen the routing map
117
- * publishes, less the template screens, the status pages and every screen
118
- * that is not public — through the same shared predicates the tenant's
119
- * `sitemap.xml` and `/llms.txt` apply, so the audit covers exactly the pages
120
- * a crawler is handed. Then each page's published version, and the shared
121
- * layouts, for the links that decide which pages nothing points to.
122
- */
123
- export declare function scanAiSeoSite(firestore: Firestore, hostId: string, host: SeoHostDocument, rawKeywords: unknown): Promise<AiSeoSiteScan>;
124
103
  /** One unit of generated work an audit still owes. */
125
104
  export type AiSeoAuditUnit = {
126
105
  kind: 'site';