@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.247

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 (173) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +7 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +21 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +98 -23
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  16. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  17. package/src/lib/declarations.server.js +12 -0
  18. package/src/lib/declarations.server.js.map +1 -1
  19. package/src/lib/jobs/ai-build-capabilities.js +216 -2
  20. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  21. package/src/lib/jobs/ai-job-build-step.d.ts +3 -1
  22. package/src/lib/jobs/ai-job-build-step.js +124 -18
  23. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  25. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  26. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-form-dataset.d.ts +8 -5
  28. package/src/lib/jobs/ai-job-form-dataset.js +28 -10
  29. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
  30. package/src/lib/jobs/ai-job-form-step.d.ts +20 -4
  31. package/src/lib/jobs/ai-job-form-step.js +98 -28
  32. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-layout-language.d.ts +6 -2
  34. package/src/lib/jobs/ai-job-layout-language.js +29 -5
  35. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  36. package/src/lib/jobs/ai-job-layout-step.js +4 -1
  37. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  38. package/src/lib/jobs/ai-job-media-step.d.ts +239 -0
  39. package/src/lib/jobs/ai-job-media-step.js +610 -0
  40. package/src/lib/jobs/ai-job-media-step.js.map +1 -0
  41. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  42. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  43. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  44. package/src/lib/jobs/ai-job-page-language.d.ts +4 -1
  45. package/src/lib/jobs/ai-job-page-language.js +30 -5
  46. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-page-sections.js +5 -4
  48. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-step.js +3 -2
  50. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  51. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -3
  52. package/src/lib/jobs/ai-job-plan-step.js +87 -20
  53. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  55. package/src/lib/jobs/ai-job-products-step.js +2 -2
  56. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-settings-step.d.ts +268 -0
  58. package/src/lib/jobs/ai-job-settings-step.js +690 -0
  59. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  60. package/src/lib/jobs/ai-job-site-content.d.ts +22 -8
  61. package/src/lib/jobs/ai-job-site-content.js +33 -15
  62. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-datasets.d.ts +3 -1
  64. package/src/lib/jobs/ai-job-site-datasets.js +13 -5
  65. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  66. package/src/lib/jobs/ai-job-site-events.d.ts +148 -0
  67. package/src/lib/jobs/ai-job-site-events.js +313 -0
  68. package/src/lib/jobs/ai-job-site-events.js.map +1 -0
  69. package/src/lib/jobs/ai-job-site-step.d.ts +12 -4
  70. package/src/lib/jobs/ai-job-site-step.js +132 -26
  71. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-tracking-step.d.ts +184 -0
  73. package/src/lib/jobs/ai-job-tracking-step.js +501 -0
  74. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -0
  75. package/src/lib/jobs/ai-layout-stock-photos.d.ts +21 -0
  76. package/src/lib/jobs/ai-layout-stock-photos.js +41 -4
  77. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  78. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  79. package/src/lib/jobs/ai-site-publish.js +202 -56
  80. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  81. package/src/lib/jobs/ai-stock-photo-picker.d.ts +24 -0
  82. package/src/lib/jobs/ai-stock-photo-picker.js +108 -0
  83. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -0
  84. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.d.ts +37 -0
  85. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js +47 -0
  86. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js.map +1 -0
  87. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.d.ts +21 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js +128 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js.map +1 -0
  90. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -7
  91. package/src/lib/layout-language/ai-layout-compiler.js +62 -42
  92. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  93. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  94. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  95. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  96. package/src/lib/layout-language/ai-layout-frame.d.ts +24 -5
  97. package/src/lib/layout-language/ai-layout-frame.js +93 -20
  98. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  99. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  101. package/src/lib/layout-language/ai-layout-labels.d.ts +70 -0
  102. package/src/lib/layout-language/ai-layout-labels.js +162 -0
  103. package/src/lib/layout-language/ai-layout-labels.js.map +1 -0
  104. package/src/lib/layout-language/ai-layout-links.js +5 -2
  105. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  106. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  107. package/src/lib/layout-language/ai-layout-listings.js +66 -5
  108. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  109. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  110. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  111. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  112. package/src/lib/model/ai-build-event-page.d.ts +70 -0
  113. package/src/lib/model/ai-build-event-page.js +145 -0
  114. package/src/lib/model/ai-build-event-page.js.map +1 -0
  115. package/src/lib/model/ai-build-job.d.ts +44 -0
  116. package/src/lib/model/ai-build-job.js +141 -13
  117. package/src/lib/model/ai-build-job.js.map +1 -1
  118. package/src/lib/model/ai-build-plan.d.ts +39 -3
  119. package/src/lib/model/ai-build-plan.js +59 -5
  120. package/src/lib/model/ai-build-plan.js.map +1 -1
  121. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  122. package/src/lib/model/ai-credit-estimate.js +51 -0
  123. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  124. package/src/lib/model/ai-job-activity.js +6 -1
  125. package/src/lib/model/ai-job-activity.js.map +1 -1
  126. package/src/lib/model/ai-jobs.types.d.ts +18 -2
  127. package/src/lib/model/ai-jobs.types.js +6 -1
  128. package/src/lib/model/ai-jobs.types.js.map +1 -1
  129. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  130. package/src/lib/model/ai-layout-menus.js +518 -0
  131. package/src/lib/model/ai-layout-menus.js.map +1 -0
  132. package/src/lib/model/ai-plan-capabilities.d.ts +8 -0
  133. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  134. package/src/lib/model/ai-products.d.ts +31 -1
  135. package/src/lib/model/ai-products.js +34 -1
  136. package/src/lib/model/ai-products.js.map +1 -1
  137. package/src/lib/model/ai-site-inventory.d.ts +17 -0
  138. package/src/lib/model/ai-site-inventory.js +5 -0
  139. package/src/lib/model/ai-site-inventory.js.map +1 -1
  140. package/src/lib/model/ai-site-job.d.ts +12 -2
  141. package/src/lib/model/ai-site-job.js +39 -12
  142. package/src/lib/model/ai-site-job.js.map +1 -1
  143. package/src/lib/model/ai-site-store-pages.js +1 -1
  144. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  145. package/src/lib/runtime/ai-doctrine-validators.d.ts +78 -3
  146. package/src/lib/runtime/ai-doctrine-validators.js +219 -31
  147. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  148. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  149. package/src/lib/runtime/ai-doctrine.js +16 -7
  150. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  151. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  152. package/src/lib/runtime/ai-eval-live.js +1 -1
  153. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  154. package/src/lib/runtime/ai-palette.d.ts +3 -1
  155. package/src/lib/runtime/ai-palette.generated.js +499 -11
  156. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  157. package/src/lib/runtime/ai-palette.js +5 -3
  158. package/src/lib/runtime/ai-palette.js.map +1 -1
  159. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  160. package/src/lib/runtime/ai-products-generation.js +12 -6
  161. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  162. package/src/lib/runtime/ai-template-examples.js +8 -2
  163. package/src/lib/runtime/ai-template-examples.js.map +1 -1
  164. package/src/lib/runtime/site-inventory.d.ts +12 -0
  165. package/src/lib/runtime/site-inventory.js +29 -1
  166. package/src/lib/runtime/site-inventory.js.map +1 -1
  167. package/src/lib/server.js +20 -0
  168. package/src/lib/server.js.map +1 -1
  169. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  170. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  171. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  172. package/src/lib/tools/ai-products-tool.js +15 -5
  173. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -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 {\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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"}
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 at its suggested price (the store's default where it has\n * none, Zach 2026-10-12), and a proposed discount is created 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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"}
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
18
+ import type { PluginAiCapabilityArgsSchema } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
19
+ import { syncHostProjectionForMembers } from '@aglyn/tenant-data-admin/server/host-memberships';
20
+ import { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown';
21
+ import { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations';
22
+ import { type Firestore } from 'firebase-admin/firestore';
23
+ import { type AiJobAdmission } from './ai-job-admission';
24
+ import type { AiJobStepRunner } from './ai-job-text-step';
25
+ /**
26
+ * The `settings` step (AGL-3748): a change to what a member can change in the
27
+ * site's settings — its name, its search defaults, its favicon, logo and
28
+ * social image from the media library, its language, its time zone, its
29
+ * contact details and its consent banner's mode — as a first-class build
30
+ * operation, so "rename my site to Juniper Clay Studio", "make the site
31
+ * Spanish" or "set the SEO description" is one item on Assist's plan card.
32
+ *
33
+ * NO MODEL. The planner already wrote the values as the item's arguments;
34
+ * this checks each against the rule the console's own control holds it to
35
+ * and writes it, so it costs no credits.
36
+ *
37
+ * ── Only the console's own fields ────────────────────────────────────────
38
+ *
39
+ * Every field written is one a settings card or form writes, under the same
40
+ * name and to the same limit: `displayName` (Admin → General, 30), `seo.title`
41
+ * (60), `seo.description` (155), `seo.separator` (3), `seo.titlePattern`
42
+ * (120, its own variables only), `seo.entity.name`, `seo.entity.description`
43
+ * (300), `seo.entity.telephone`, `business.supportEmail` and
44
+ * `business.address` (the Business details card), `locales`/`defaultLocale`
45
+ * (the Languages card), `timeZone` (one of the zones the console offers),
46
+ * `consent.mode` (the consent banner card), and `seo.favicon`, `logoUrl` and
47
+ * `seo.image` (with its size and alt) written as the media pickers write
48
+ * them, through `mediaNodeSrc`.
49
+ *
50
+ * NEVER: the address (subdomain, domains), plugins, members and roles,
51
+ * billing, lockdown, deletion, search engine indexing, analytics and
52
+ * advertising ids, verification tokens, or turning the consent banner off.
53
+ * Those are not arguments, so the planner cannot ask for them.
54
+ *
55
+ * ── Live on confirm ──────────────────────────────────────────────────────
56
+ *
57
+ * A setting has no draft: the console saves it straight to the host
58
+ * document, and the tenant renders it from there. So does this, once the
59
+ * person confirms the plan (Zach, 2026-10-12: live on confirm). The write
60
+ * drops the site's cached pages as the console's door does
61
+ * (`host-document-writes.ts`), with a site-wide publish outbox entry in the
62
+ * same transaction for the drain to fire if the drop does not land.
63
+ *
64
+ * ── Who may ──────────────────────────────────────────────────────────────
65
+ *
66
+ * The Firestore rules' own for these keys (`canWriteHostContent` and
67
+ * `hostWritesFrozen`): a member with a write role on the site (admin, editor
68
+ * or author), while no lockdown freezes the site, its org or the platform.
69
+ * Asked at the door, again before the write, and the role once more inside
70
+ * the transaction that writes.
71
+ *
72
+ * ── Idempotent ───────────────────────────────────────────────────────────
73
+ *
74
+ * The transaction compares each value with what is stored and writes only
75
+ * what differs, so a pass asked again writes nothing and announces nothing.
76
+ */
77
+ export declare const AI_SETTINGS_KIND: "settings";
78
+ /**
79
+ * ASSUMED: the least time the step needs — the site, the org, up to three
80
+ * media lookups in two libraries, and the one transaction that writes. No
81
+ * model, so it fits an inline door.
82
+ */
83
+ export declare const AI_JOB_SETTINGS_STEP_MINIMUM_MS = 5000;
84
+ export declare const AI_SETTINGS_NO_SITE_COPY = "Open the site whose settings should change.";
85
+ export declare const AI_SETTINGS_ROLE_COPY = "Changing the site\u2019s settings needs the admin, editor or author role on this site.";
86
+ export declare const AI_SETTINGS_LOCKED_COPY = "This site is not accepting changes right now.";
87
+ export declare const AI_SETTINGS_NOTHING_COPY = "The request named no site setting this could change.";
88
+ /** Where a person finds what the op changed, as the build's notes name it. */
89
+ export declare const AI_SETTINGS_WHERE = "the site\u2019s Settings";
90
+ /** The output's fixed id: one site's settings, whatever changed. */
91
+ export declare const AI_SETTINGS_OUTPUT_ID = "site";
92
+ /** The console's own limits, each from the control that edits the field. */
93
+ export declare const AI_SETTINGS_LIMITS: {
94
+ /** Admin → General, Display name. */
95
+ readonly name: 30;
96
+ /** Setup → SEO, Title. */
97
+ readonly seoTitle: 60;
98
+ /** Setup → SEO, Description. */
99
+ readonly seoDescription: 155;
100
+ /** Setup → SEO, Separator. */
101
+ readonly separator: 3;
102
+ /** Setup → SEO, Title pattern. */
103
+ readonly titlePattern: 120;
104
+ /** Setup → SEO, the publisher's description. */
105
+ readonly businessDescription: 300;
106
+ readonly businessName: 100;
107
+ readonly telephone: 40;
108
+ readonly supportEmail: 254;
109
+ readonly address: 200;
110
+ };
111
+ /** The consent banner card's two modes; turning the banner off is not offered. */
112
+ export declare const AI_SETTINGS_CONSENT_MODES: readonly ["geo", "strict"];
113
+ /** The media a setting may name, and the field each is written to. */
114
+ export declare const AI_SETTINGS_MEDIA_ARGS: readonly ["favicon", "logo", "socialImage"];
115
+ export type AiSettingsMediaArg = (typeof AI_SETTINGS_MEDIA_ARGS)[number];
116
+ /** The item's arguments, as the planner fills them in. Every one optional. */
117
+ export declare const AI_SETTINGS_ARGS_SCHEMA: PluginAiCapabilityArgsSchema;
118
+ /** What an item asks, each value read and held to the console's rule. */
119
+ export interface AiSettingsRequest {
120
+ name?: string;
121
+ seoTitle?: string;
122
+ seoDescription?: string;
123
+ separator?: string;
124
+ titlePattern?: string;
125
+ businessName?: string;
126
+ businessDescription?: string;
127
+ telephone?: string;
128
+ supportEmail?: string;
129
+ address?: string;
130
+ /** A language code the Languages card would accept. */
131
+ language?: string;
132
+ timeZone?: string;
133
+ consentMode?: (typeof AI_SETTINGS_CONSENT_MODES)[number];
134
+ favicon?: string;
135
+ logo?: string;
136
+ socialImage?: string;
137
+ }
138
+ export interface AiSettingsRead {
139
+ request: AiSettingsRequest;
140
+ /** A sentence per value that could not be used, so the rest still are. */
141
+ problems: string[];
142
+ }
143
+ /** Whether a title pattern names only the variables the renderer knows. */
144
+ export declare function aiSettingsTitlePatternOk(pattern: string): boolean;
145
+ /** The item's arguments read: what may be written, and a sentence for each that may not. */
146
+ export declare function aiSettingsRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSettingsRead;
147
+ /** Whether a read request asks for anything at all. */
148
+ export declare function aiSettingsRequestEmpty(request: AiSettingsRequest): boolean;
149
+ /** A media library row, as a setting may name it. */
150
+ export interface AiSettingsMediaRow {
151
+ id: string;
152
+ fileName: string;
153
+ contentType: string;
154
+ url: string | null;
155
+ cdnPath: string | null;
156
+ contentHash: string | null;
157
+ private: boolean;
158
+ deleted: boolean;
159
+ /** The org library's sites; `undefined` on the site's own library. */
160
+ visibleTo?: readonly string[] | null;
161
+ width: number | null;
162
+ height: number | null;
163
+ alt: string;
164
+ }
165
+ /** The image a setting names, as written to the host. */
166
+ export interface AiSettingsMedia {
167
+ src: string;
168
+ width: number | null;
169
+ height: number | null;
170
+ alt: string;
171
+ fileName: string;
172
+ }
173
+ /**
174
+ * The rows a media name could be, from the site's own library and then its
175
+ * org's: the one whose id it is, else those whose file name it is.
176
+ */
177
+ export declare function readAiSettingsMediaRows(firestore: Firestore, input: {
178
+ hostId: string;
179
+ orgId: string;
180
+ source: string;
181
+ }): Promise<AiSettingsMediaRow[]>;
182
+ /**
183
+ * The image a setting names, chosen from the rows read: a public image of
184
+ * this site's library, or of its org's that this site may use, never deleted.
185
+ * Its id wins over a file name; two images of one name are refused in words.
186
+ */
187
+ export declare function aiSettingsMediaOf(arg: AiSettingsMediaArg, source: string, rows: readonly AiSettingsMediaRow[], hostId: string): AiSettingsMedia | {
188
+ error: string;
189
+ };
190
+ /** One change to the host document, with the sentence that says it was made. */
191
+ interface AiSettingsChange {
192
+ path: string;
193
+ value: unknown;
194
+ said: string;
195
+ }
196
+ /**
197
+ * The language change, as the Languages card would save it: a site with no
198
+ * languages, or one, now publishes in this one; a multilingual site that
199
+ * lists it makes it the default. A multilingual site that does not list it
200
+ * is refused, because adding a language is the Languages card's decision.
201
+ */
202
+ export declare function aiSettingsLanguageChanges(host: {
203
+ locales?: unknown;
204
+ defaultLocale?: unknown;
205
+ }, language: string): AiSettingsChange[] | {
206
+ error: string;
207
+ };
208
+ /** Every change the request asks of this host, before comparing with what is stored. */
209
+ export declare function aiSettingsChanges(host: Record<string, unknown>, request: AiSettingsRequest, media: Partial<Record<AiSettingsMediaArg, AiSettingsMedia>>): {
210
+ changes: AiSettingsChange[];
211
+ problems: string[];
212
+ };
213
+ export type AiSettingsWrite = {
214
+ ok: true;
215
+ done: string[];
216
+ notDone: string[];
217
+ changed: string[];
218
+ outboxId: string | null;
219
+ } | {
220
+ ok: false;
221
+ error: string;
222
+ };
223
+ /**
224
+ * The settings written, in one transaction that reads the host as it is
225
+ * stored NOW: the member still holds a write role, and only the values that
226
+ * differ from what is stored are written, with a site-wide publish outbox
227
+ * entry beside them so the live site's cached pages are dropped.
228
+ */
229
+ export declare function writeAiSiteSettings(firestore: Firestore, input: {
230
+ hostId: string;
231
+ uid: string;
232
+ request: AiSettingsRequest;
233
+ media: Partial<Record<AiSettingsMediaArg, AiSettingsMedia>>;
234
+ now: Date;
235
+ }): Promise<AiSettingsWrite>;
236
+ /** Why this member may not change this site's settings now, or `null`. */
237
+ export declare function aiSettingsAccessRefusal(input: {
238
+ host: FirebaseFirestore.DocumentSnapshot;
239
+ uid: string;
240
+ org: object | null;
241
+ }, deps?: {
242
+ lockdown?: typeof getLockdownVerdict;
243
+ }): Promise<string | null>;
244
+ /** The output's note: what changed, that it is live, and what was not done. */
245
+ export declare function aiSettingsOutputNote(done: readonly string[], notDone: readonly string[], language: boolean): string;
246
+ export interface AiJobSettingsStepDeps {
247
+ readMedia?: typeof readAiSettingsMediaRows;
248
+ write?: typeof writeAiSiteSettings;
249
+ lockdown?: typeof getLockdownVerdict;
250
+ dropCache?: typeof dropPluginSiteCache;
251
+ syncMemberships?: typeof syncHostProjectionForMembers;
252
+ }
253
+ export declare function createAiJobSettingsStep(deps?: AiJobSettingsStepDeps): AiJobStepRunner;
254
+ export declare const runAiJobSettingsStep: AiJobStepRunner;
255
+ export interface AiSettingsJobAdmissionDeps {
256
+ ownerOf?: typeof resolveOrgIdForHost;
257
+ lockdown?: typeof getLockdownVerdict;
258
+ }
259
+ /**
260
+ * A settings change is admitted on a site of the job's own org, for a member
261
+ * who may write its settings now, where the request names at least one
262
+ * setting this can change. Refused in those words before anything is written.
263
+ */
264
+ export declare function createAiSettingsJobAdmission(deps?: AiSettingsJobAdmissionDeps): AiJobAdmission;
265
+ export declare const aiSettingsJobAdmission: AiJobAdmission;
266
+ /** Registers the settings step and its door (AGL-3748). */
267
+ export declare function registerAiSettingsJob(): void;
268
+ export {};