@aglyn/plugins-ai 1.0.0-beta.217 → 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 (187) 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 +10 -2
  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.d.ts +9 -1
  49. package/src/lib/jobs/ai-job-drafts.js +12 -5
  50. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  51. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  52. package/src/lib/jobs/ai-job-email-step.js +1 -3
  53. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  56. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  58. package/src/lib/jobs/ai-job-generation.js +10 -2
  59. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  61. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  63. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  64. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  65. package/src/lib/jobs/ai-job-page-step.js +7 -2
  66. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  68. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  69. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  70. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  71. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  73. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  74. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  75. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  76. package/src/lib/jobs/ai-job-products-step.js +54 -32
  77. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  78. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  79. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  80. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  81. package/src/lib/jobs/ai-job-template-step.js +1 -0
  82. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  85. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  87. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  88. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  89. package/src/lib/jobs/ai-workflow-records.js +60 -27
  90. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  91. package/src/lib/model/ai-allotments.d.ts +11 -3
  92. package/src/lib/model/ai-allotments.js +21 -4
  93. package/src/lib/model/ai-allotments.js.map +1 -1
  94. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  95. package/src/lib/model/ai-automation-outline.js.map +1 -1
  96. package/src/lib/model/ai-build-plan.d.ts +57 -0
  97. package/src/lib/model/ai-build-plan.js +116 -7
  98. package/src/lib/model/ai-build-plan.js.map +1 -1
  99. package/src/lib/model/ai-seo.d.ts +11 -51
  100. package/src/lib/model/ai-seo.js +0 -17
  101. package/src/lib/model/ai-seo.js.map +1 -1
  102. package/src/lib/model/ai-site-inventory.js +1 -1
  103. package/src/lib/model/ai-site-inventory.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.js +2 -2
  105. package/src/lib/model/ai-template-subjects.js.map +1 -1
  106. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  107. package/src/lib/model/ai-theme-proposal.js +1 -1
  108. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  109. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  110. package/src/lib/model/ai-workflow-job.js +1 -2
  111. package/src/lib/model/ai-workflow-job.js.map +1 -1
  112. package/src/lib/plan-entitlements.d.ts +149 -0
  113. package/src/lib/plan-entitlements.js +168 -0
  114. package/src/lib/plan-entitlements.js.map +1 -0
  115. package/src/lib/plugin.js +1 -1
  116. package/src/lib/plugin.js.map +1 -1
  117. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  118. package/src/lib/runtime/ai-doctrine-validators.js +179 -23
  119. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  120. package/src/lib/runtime/ai-doctrine.js +5 -5
  121. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  122. package/src/lib/runtime/ai-eval-live.js +29 -14
  123. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  124. package/src/lib/runtime/ai-eval.d.ts +8 -1
  125. package/src/lib/runtime/ai-eval.js +29 -2
  126. package/src/lib/runtime/ai-eval.js.map +1 -1
  127. package/src/lib/runtime/ai-node-tree.js +3 -3
  128. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  129. package/src/lib/runtime/ai-palette.d.ts +6 -0
  130. package/src/lib/runtime/ai-palette.generated.js +21 -255
  131. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  132. package/src/lib/runtime/ai-palette.js +7 -1
  133. package/src/lib/runtime/ai-palette.js.map +1 -1
  134. package/src/lib/runtime/seo-audit.d.ts +13 -63
  135. package/src/lib/runtime/seo-audit.js +22 -207
  136. package/src/lib/runtime/seo-audit.js.map +1 -1
  137. package/src/lib/runtime/site-inventory.js +1 -1
  138. package/src/lib/runtime/site-inventory.js.map +1 -1
  139. package/src/lib/server/ai-admin-org.js +4 -25
  140. package/src/lib/server/ai-admin-org.js.map +1 -1
  141. package/src/lib/server/ai-admin-user.js +2 -19
  142. package/src/lib/server/ai-admin-user.js.map +1 -1
  143. package/src/lib/server/ai-assist-prompts.js +3 -3
  144. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  145. package/src/lib/server/ai-insight-digest-route.js +8 -1
  146. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  147. package/src/lib/server/assist-chat.js +11 -10
  148. package/src/lib/server/assist-chat.js.map +1 -1
  149. package/src/lib/server/assist-view-context.js +23 -23
  150. package/src/lib/server/assist-view-context.js.map +1 -1
  151. package/src/lib/server.d.ts +1 -0
  152. package/src/lib/server.js.map +1 -1
  153. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  154. package/src/lib/testing/stand-in-automation-index.js +102 -0
  155. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  156. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  157. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  158. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  159. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  160. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  161. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  162. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  163. package/src/lib/testing/stand-in-product-index.js +105 -0
  164. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  165. package/src/lib/tools/ai-component-tool.js +2 -2
  166. package/src/lib/tools/ai-component-tool.js.map +1 -1
  167. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  168. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  169. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  170. package/src/lib/tools/ai-seo-tool.js +7 -38
  171. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  172. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  173. package/src/lib/tools/ai-workflow-tool.js +1 -1
  174. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  175. package/src/lib/usage/ai-allotment-alerts.d.ts +3 -0
  176. package/src/lib/usage/ai-allotment-alerts.js +20 -8
  177. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  178. package/src/lib/usage/assist-free-taste.js +2 -1
  179. package/src/lib/usage/assist-free-taste.js.map +1 -1
  180. package/src/lib/usage/assist-signal-mining.js +1 -1
  181. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  182. package/src/lib/usage-axes.d.ts +39 -0
  183. package/src/lib/usage-axes.js +70 -0
  184. package/src/lib/usage-axes.js.map +1 -0
  185. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  186. package/src/lib/runtime/seo-page-facts.js +0 -150
  187. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-email-bindings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n campaignSendTimeLabel,\n suggestCampaignSendTime,\n type CampaignSendTimeSuggestion,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-send-time'\nimport { orgDataCollectionForHost } from '@aglyn/tenant-data-admin/server/organizations'\n\n/**\n * What an email or campaign job binds and suggests WITHOUT the model\n * (AGL-2912).\n *\n * What AI generation is published to send is the brief, a summary of the\n * site and the content being worked on — not email lists, contacts,\n * recipients, CRM records, product records or engagement statistics. So\n * everything here that touches one of those runs in code, on the server, and\n * hands the model at most a count:\n *\n * - PRODUCTS are bound by id: the ones the member picked for the job, else\n * the ones the brief names by their exact name. The model learns only how\n * many product blocks to place; the ids are set on those blocks after it\n * answers, and the send fills each block's name, price and picture.\n * - THE LIST a campaign is for is suggested when the brief names one of the\n * org's lists by name, and is set on nothing: the drafted email is aimed at\n * nobody until a person chooses.\n * - THE SEND TIME is the shared rule over that list's past sends on this\n * site — when each went out, how many were delivered and how many people\n * opened it — and is said only in the output's note.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The most products one email binds. */\nexport const AI_EMAIL_MAX_PRODUCTS = 6\n\n/** How many of a site's products are read for the names a brief might use. */\nexport const AI_EMAIL_PRODUCT_SCAN = 200\n\n/** How many of an org's lists are read: the composer's list picker window. */\nexport const AI_CAMPAIGN_LIST_SCAN = 50\n\n/** How many of a list's past sends on the site the send time is taken from. */\nexport const AI_CAMPAIGN_SEND_HISTORY_SCAN = 50\n\n/** A record a brief may name, by its id and its name. */\nexport interface AiNamedRecord {\n id: string\n name: string\n}\n\n/** Lowercase words joined by single spaces, for matching a name inside text. */\nfunction wordsOf(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The records a text names by their whole name, in the order it names them.\n * A name must stand as its own words (\"Rye loaf\" is not named by \"rye\n * loaves\"), a longer name takes the words it shares with a shorter one (\"Rye\n * loaf\" over \"Rye\"), and a name shorter than three characters names nothing.\n */\nexport function aiRecordsNamedIn(\n text: string,\n records: readonly AiNamedRecord[],\n max: number,\n): AiNamedRecord[] {\n const haystack = ` ${wordsOf(text)} `\n const hits: Array<{ record: AiNamedRecord; at: number; end: number }> = []\n for (const record of records) {\n const needle = wordsOf(record.name)\n if (needle.length < 3) continue\n const at = haystack.indexOf(` ${needle} `)\n if (at !== -1) hits.push({ record, at, end: at + needle.length + 1 })\n }\n const kept: typeof hits = []\n for (const hit of [...hits].sort((a, b) => b.end - b.at - (a.end - a.at))) {\n if (kept.some((other) => hit.at < other.end && hit.end > other.at)) continue\n kept.push(hit)\n }\n return kept\n .sort((a, b) => a.at - b.at)\n .map((hit) => hit.record)\n .slice(0, Math.max(0, max))\n}\n\n/**\n * The product ids a member picked for the job, as the create door stores a\n * scalar input: one string of ids separated by commas or spaces.\n */\nexport function aiEmailPickedProductIds(inputs: Readonly<Record<string, unknown>> | undefined): string[] {\n const raw = inputs?.['productIds']\n if (typeof raw !== 'string') return []\n const ids = raw.split(/[\\s,]+/).filter((id) => /^[A-Za-z0-9_-]{1,128}$/.test(id))\n return [...new Set(ids)].slice(0, AI_EMAIL_MAX_PRODUCTS)\n}\n\nexport interface AiEmailProductBinding {\n /** The product ids bound, in the order the email's product blocks take them. */\n ids: string[]\n /** How many the member picked; 0 when the ids came from the brief. */\n picked: number\n}\n\n/**\n * The products an email binds: the member's picks that still exist, else the\n * ones the brief names. Reads ids and names, and returns only ids.\n */\nexport async function resolveAiEmailProducts(\n firestore: Firestore,\n input: { hostId: string; brief: string; inputs: Readonly<Record<string, unknown>> | undefined },\n): Promise<AiEmailProductBinding> {\n const products = firestore.collection('hosts').doc(input.hostId).collection('products')\n const picked = aiEmailPickedProductIds(input.inputs)\n if (picked.length) {\n const snapshots = await Promise.all(picked.map((id) => products.doc(id).get()))\n return {\n ids: snapshots\n .filter((snapshot) => snapshot.exists && snapshot.get('deletedAt') == null)\n .map((snapshot) => snapshot.id),\n picked: picked.length,\n }\n }\n const snapshot = await products.select('name', 'deletedAt').limit(AI_EMAIL_PRODUCT_SCAN).get()\n const rows = snapshot.docs\n .filter((doc) => doc.get('deletedAt') == null)\n .map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return {\n ids: aiRecordsNamedIn(input.brief, rows, AI_EMAIL_MAX_PRODUCTS).map((row) => row.id),\n picked: 0,\n }\n}\n\n/**\n * The org list a campaign's brief names, found the way the send path finds an\n * org's lists; `null` when it names none.\n */\nexport async function suggestAiCampaignList(input: {\n hostId: string\n brief: string\n}): Promise<AiNamedRecord | null> {\n const contacts = await orgDataCollectionForHost(input.hostId, 'contacts')\n const lists = contacts.parent?.collection('lists')\n if (!lists) return null\n const snapshot = await lists.select('name').limit(AI_CAMPAIGN_LIST_SCAN).get()\n const rows = snapshot.docs.map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return aiRecordsNamedIn(input.brief, rows, 1)[0] ?? null\n}\n\nfunction millisOf(value: unknown): number {\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n const toMillis = (value as { toMillis?: () => number } | null)?.toMillis\n return typeof toMillis === 'function' ? toMillis.call(value) : 0\n}\n\n/**\n * The send time a list's past sends on this site suggest, or `null` with too\n * little history.\n *\n * Sends are the organization's (`orgs/{orgId}/campaigns`), and a list is too,\n * so the org's sends to the list are narrowed to the ones sent AS this site\n * (`hostId`): another site's audience opening at another hour is not this\n * site's history. Three equality filters, which Firestore serves by merging\n * single-field indexes — no composite index.\n */\nexport async function readAiListSendTime(\n firestore: Firestore,\n input: { orgId: string; hostId: string; listId: string },\n): Promise<CampaignSendTimeSuggestion | null> {\n if (!input.orgId || !input.hostId) return null\n const snapshot = await firestore\n .collection('orgs')\n .doc(input.orgId)\n .collection('campaigns')\n .where('hostId', '==', input.hostId)\n .where('listId', '==', input.listId)\n .where('status', '==', 'sent')\n .select('sentAt', 'stats.delivered', 'stats.uniqueOpens')\n .limit(AI_CAMPAIGN_SEND_HISTORY_SCAN)\n .get()\n return suggestCampaignSendTime(\n snapshot.docs.map((doc) => ({\n sentAtMs: millisOf(doc.get('sentAt')),\n delivered: Number(doc.get('stats.delivered') ?? 0),\n uniqueOpens: Number(doc.get('stats.uniqueOpens') ?? 0),\n })),\n )\n}\n\n/** What an email's output says about its products; `null` when it binds none and none were picked. */\nexport function aiEmailProductNote(binding: AiEmailProductBinding): string | null {\n const bound = binding.ids.length\n const missing = binding.picked - bound\n const parts: string[] = []\n if (bound === 1) {\n parts.push('Its product card shows the product’s current name, price and picture when it is sent.')\n } else if (bound > 1) {\n parts.push(`Its ${bound} product cards show each product’s current name, price and picture when it is sent.`)\n }\n if (missing > 0) {\n parts.push(\n `${missing} of the products picked no longer ${missing === 1 ? 'exists' : 'exist'}, so the email places ${\n bound || 'none'\n }.`,\n )\n }\n return parts.length ? parts.join(' ') : null\n}\n\n/** What a campaign's output says about who it is for and when to send it. */\nexport function aiCampaignAudienceNote(\n list: AiNamedRecord | null,\n sendTime: CampaignSendTimeSuggestion | null,\n): string {\n if (!list) {\n return 'The email is aimed at nobody yet: choose who receives it before you schedule it.'\n }\n const when = sendTime\n ? ` Past campaigns to that list on this site were opened most when sent on ${campaignSendTimeLabel(\n sendTime,\n )}, across ${sendTime.measured} sends.`\n : ' That list has too little send history on this site to suggest a send time.'\n return `The brief names the list \"${list.name}\". The email is aimed at nobody yet: choose \"${list.name}\" as its audience before you schedule it.${when}`\n}\n"],"names":["campaignSendTimeLabel","suggestCampaignSendTime","orgDataCollectionForHost","AI_EMAIL_MAX_PRODUCTS","AI_EMAIL_PRODUCT_SCAN","AI_CAMPAIGN_LIST_SCAN","AI_CAMPAIGN_SEND_HISTORY_SCAN","wordsOf","value","toLowerCase","replace","trim","aiRecordsNamedIn","text","records","max","haystack","hits","record","needle","name","length","at","indexOf","push","end","kept","hit","sort","a","b","some","other","map","slice","Math","aiEmailPickedProductIds","inputs","raw","ids","split","filter","id","test","Set","resolveAiEmailProducts","firestore","input","products","collection","doc","hostId","picked","snapshots","Promise","all","get","snapshot","exists","select","limit","rows","docs","String","brief","row","suggestAiCampaignList","contacts","lists","parent","millisOf","Date","getTime","toMillis","call","readAiListSendTime","orgId","where","listId","sentAtMs","delivered","Number","uniqueOpens","aiEmailProductNote","binding","bound","missing","parts","join","aiCampaignAudienceNote","list","sendTime","when","measured"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-email-bindings.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 { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { pluginRecordFactsReader } from '@aglyn/aglyn/plugin-manager/plugin-record-facts'\nimport { orgDataCollectionForHost } from '@aglyn/tenant-data-admin/server/organizations'\n\n/**\n * What an email or campaign job binds and suggests WITHOUT the model\n * (AGL-2912).\n *\n * What AI generation is published to send is the brief, a summary of the\n * site and the content being worked on — not email lists, contacts,\n * recipients, CRM records, product records or engagement statistics. So\n * everything here that touches one of those runs in code, on the server, and\n * hands the model at most a count:\n *\n * - PRODUCTS are bound by id: the ones the member picked for the job, else\n * the ones the brief names by their exact name. The model learns only how\n * many product blocks to place; the ids are set on those blocks after it\n * answers, and the send fills each block's name, price and picture.\n * - THE LIST a campaign is for is suggested when the brief names one of the\n * org's lists by name, and is set on nothing: the drafted email is aimed at\n * nobody until a person chooses.\n * - THE SEND TIME is asked of the plugin that keeps the list's past sends,\n * through the core's record-facts seam, which answers the slot and how\n * many sends it was chosen among — and is said only in the output's note.\n */\n\n/** The most products one email binds. */\nexport const AI_EMAIL_MAX_PRODUCTS = 6\n\n/** How many of a site's products are read for the names a brief might use. */\nexport const AI_EMAIL_PRODUCT_SCAN = 200\n\n/** How many of an org's lists are read: the composer's list picker window. */\nexport const AI_CAMPAIGN_LIST_SCAN = 50\n\n/**\n * The resource a list's send time is read as, by the list's id, through\n * `plugin-record-facts`. Its owner answers `{ label, measured }`: the slot as\n * a person reads it (or `null` with too little history) and the sends it was\n * chosen among.\n */\nexport const AI_LIST_SEND_TIME_RESOURCE = 'listSendTime'\n\n/** A list's suggested send time, as the note says it. */\nexport interface AiListSendTime {\n label: string\n measured: number\n}\n\n/** A record a brief may name, by its id and its name. */\nexport interface AiNamedRecord {\n id: string\n name: string\n}\n\n/** Lowercase words joined by single spaces, for matching a name inside text. */\nfunction wordsOf(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The records a text names by their whole name, in the order it names them.\n * A name must stand as its own words (\"Rye loaf\" is not named by \"rye\n * loaves\"), a longer name takes the words it shares with a shorter one (\"Rye\n * loaf\" over \"Rye\"), and a name shorter than three characters names nothing.\n */\nexport function aiRecordsNamedIn(\n text: string,\n records: readonly AiNamedRecord[],\n max: number,\n): AiNamedRecord[] {\n const haystack = ` ${wordsOf(text)} `\n const hits: Array<{ record: AiNamedRecord; at: number; end: number }> = []\n for (const record of records) {\n const needle = wordsOf(record.name)\n if (needle.length < 3) continue\n const at = haystack.indexOf(` ${needle} `)\n if (at !== -1) hits.push({ record, at, end: at + needle.length + 1 })\n }\n const kept: typeof hits = []\n for (const hit of [...hits].sort((a, b) => b.end - b.at - (a.end - a.at))) {\n if (kept.some((other) => hit.at < other.end && hit.end > other.at)) continue\n kept.push(hit)\n }\n return kept\n .sort((a, b) => a.at - b.at)\n .map((hit) => hit.record)\n .slice(0, Math.max(0, max))\n}\n\n/**\n * The product ids a member picked for the job, as the create door stores a\n * scalar input: one string of ids separated by commas or spaces.\n */\nexport function aiEmailPickedProductIds(inputs: Readonly<Record<string, unknown>> | undefined): string[] {\n const raw = inputs?.['productIds']\n if (typeof raw !== 'string') return []\n const ids = raw.split(/[\\s,]+/).filter((id) => /^[A-Za-z0-9_-]{1,128}$/.test(id))\n return [...new Set(ids)].slice(0, AI_EMAIL_MAX_PRODUCTS)\n}\n\nexport interface AiEmailProductBinding {\n /** The product ids bound, in the order the email's product blocks take them. */\n ids: string[]\n /** How many the member picked; 0 when the ids came from the brief. */\n picked: number\n}\n\n/**\n * The products an email binds: the member's picks that still exist, else the\n * ones the brief names. Reads ids and names through the `product` index the\n * plugin that keeps products publishes (AGL-3080), and returns only ids; none\n * where no plugin keeps products here.\n */\nexport async function resolveAiEmailProducts(input: {\n hostId: string\n brief: string\n inputs: Readonly<Record<string, unknown>> | undefined\n}): Promise<AiEmailProductBinding> {\n const picked = aiEmailPickedProductIds(input.inputs)\n const index = pluginRecordIndex('product')?.index\n if (!index) return { ids: [], picked: picked.length }\n if (picked.length) {\n const records = await Promise.all(picked.map((id) => index.get({ hostId: input.hostId, id })))\n return {\n ids: records.filter((record) => record !== null).map((record) => record.id),\n picked: picked.length,\n }\n }\n const { records } = await index.list({ hostId: input.hostId, limit: AI_EMAIL_PRODUCT_SCAN })\n return {\n ids: aiRecordsNamedIn(\n input.brief,\n records.map((record) => ({ id: record.id, name: record.name })),\n AI_EMAIL_MAX_PRODUCTS,\n ).map((row) => row.id),\n picked: 0,\n }\n}\n\n/**\n * The org list a campaign's brief names, found the way the send path finds an\n * org's lists; `null` when it names none.\n */\nexport async function suggestAiCampaignList(input: {\n hostId: string\n brief: string\n}): Promise<AiNamedRecord | null> {\n const contacts = await orgDataCollectionForHost(input.hostId, 'contacts')\n const lists = contacts.parent?.collection('lists')\n if (!lists) return null\n const snapshot = await lists.select('name').limit(AI_CAMPAIGN_LIST_SCAN).get()\n const rows = snapshot.docs.map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return aiRecordsNamedIn(input.brief, rows, 1)[0] ?? null\n}\n\n/**\n * The send time a list's past sends on this site suggest, asked of the plugin\n * that keeps the sends: `null` when nothing keeps them here, when the owner\n * refuses, or when there is too little history to suggest one.\n */\nexport async function readAiListSendTime(input: {\n orgId: string\n hostId: string\n listId: string\n uid: string\n org: Readonly<Record<string, unknown>> | null\n now: Date\n}): Promise<AiListSendTime | null> {\n if (!input.orgId || !input.hostId || !input.listId) return null\n const owner = pluginRecordFactsReader(AI_LIST_SEND_TIME_RESOURCE)\n if (!owner) return null\n const read = await owner.reader.read({\n orgId: input.orgId,\n hostId: input.hostId,\n id: input.listId,\n uid: input.uid,\n org: input.org,\n now: input.now,\n })\n if (!read.ok) return null\n const label = read.facts['label']\n const measured = Number(read.facts['measured'] ?? 0)\n return typeof label === 'string' && label && measured > 0 ? { label, measured } : null\n}\n\n/** What an email's output says about its products; `null` when it binds none and none were picked. */\nexport function aiEmailProductNote(binding: AiEmailProductBinding): string | null {\n const bound = binding.ids.length\n const missing = binding.picked - bound\n const parts: string[] = []\n if (bound === 1) {\n parts.push('Its product card shows the product’s current name, price and picture when it is sent.')\n } else if (bound > 1) {\n parts.push(`Its ${bound} product cards show each product’s current name, price and picture when it is sent.`)\n }\n if (missing > 0) {\n parts.push(\n `${missing} of the products picked no longer ${missing === 1 ? 'exists' : 'exist'}, so the email places ${\n bound || 'none'\n }.`,\n )\n }\n return parts.length ? parts.join(' ') : null\n}\n\n/** What a campaign's output says about who it is for and when to send it. */\nexport function aiCampaignAudienceNote(\n list: AiNamedRecord | null,\n sendTime: AiListSendTime | null,\n): string {\n if (!list) {\n return 'The email is aimed at nobody yet: choose who receives it before you schedule it.'\n }\n const when = sendTime\n ? ` Past campaigns to that list on this site were opened most when sent on ${sendTime.label}, across ${sendTime.measured} sends.`\n : ' That list has too little send history on this site to suggest a send time.'\n return `The brief names the list \"${list.name}\". The email is aimed at nobody yet: choose \"${list.name}\" as its audience before you schedule it.${when}`\n}\n"],"names":["pluginRecordIndex","pluginRecordFactsReader","orgDataCollectionForHost","AI_EMAIL_MAX_PRODUCTS","AI_EMAIL_PRODUCT_SCAN","AI_CAMPAIGN_LIST_SCAN","AI_LIST_SEND_TIME_RESOURCE","wordsOf","value","toLowerCase","replace","trim","aiRecordsNamedIn","text","records","max","haystack","hits","record","needle","name","length","at","indexOf","push","end","kept","hit","sort","a","b","some","other","map","slice","Math","aiEmailPickedProductIds","inputs","raw","ids","split","filter","id","test","Set","resolveAiEmailProducts","input","picked","index","Promise","all","get","hostId","list","limit","brief","row","suggestAiCampaignList","contacts","lists","parent","collection","snapshot","select","rows","docs","doc","String","readAiListSendTime","read","orgId","listId","owner","reader","uid","org","now","ok","label","facts","measured","Number","aiEmailProductNote","binding","bound","missing","parts","join","aiCampaignAudienceNote","sendTime","when"],"mappings":"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"}
@@ -56,8 +56,6 @@ import type { AiJobStepRunner } from './ai-job-text-step';
56
56
  * reported, a design alone is drafted into a campaign from the copy the
57
57
  * design already stores, and neither spends.
58
58
  */
59
- /** The id the marketing plugin is registered under. */
60
- export declare const AI_CAMPAIGN_PLUGIN_ID = "marketing";
61
59
  /** The resource the marketing plugin writes a draft campaign under. */
62
60
  export declare const AI_CAMPAIGN_RESOURCE = "campaign";
63
61
  /** A campaign's name when neither the job nor its subject lines give one. */
@@ -22,7 +22,7 @@ import { registerAiJobAdmission } from "./ai-job-admission.js";
22
22
  import { aiJobDraftId } from "./ai-job-draft-ids.js";
23
23
  import { aiSiteSubdomain } from "./ai-job-drafts.js";
24
24
  import { aiConfirmedPlan, aiDoctrineReview, aiGenerationSpent, aiLimitReview, aiUnspentOutcome } from "./ai-job-generation.js";
25
- import { AI_EMAIL_DESIGN_RESOURCE, AI_EMAIL_PLUGIN_ID, AI_EMAIL_SAVE_FAILURE_COPY, AI_JOB_EMAIL_STEP_BUDGETS, aiEmailDesignContent, aiEmailDesignOutput, aiJobInputName, generateAiEmail } from "./ai-job-email-step.js";
25
+ import { AI_EMAIL_DESIGN_RESOURCE, AI_EMAIL_SAVE_FAILURE_COPY, AI_JOB_EMAIL_STEP_BUDGETS, aiEmailDesignContent, aiEmailDesignOutput, aiJobInputName, generateAiEmail } from "./ai-job-email-step.js";
26
26
  import { aiPluginDraftAdmissionRefusal, aiPluginDraftWriter } from "./ai-job-plugin-drafts.js";
27
27
  import { registerAiJobStep } from "./ai-jobs.js";
28
28
  /**
@@ -59,8 +59,7 @@ import { registerAiJobStep } from "./ai-jobs.js";
59
59
  * created, so a run cut off between them finds what it wrote: two drafts are
60
60
  * reported, a design alone is drafted into a campaign from the copy the
61
61
  * design already stores, and neither spends.
62
- */ /** The id the marketing plugin is registered under. */ export const AI_CAMPAIGN_PLUGIN_ID = 'marketing';
63
- /** The resource the marketing plugin writes a draft campaign under. */ export const AI_CAMPAIGN_RESOURCE = 'campaign';
62
+ */ /** The resource the marketing plugin writes a draft campaign under. */ export const AI_CAMPAIGN_RESOURCE = 'campaign';
64
63
  /** A campaign's name when neither the job nor its subject lines give one. */ export const AI_JOB_CAMPAIGN_DEFAULT_NAME = 'New campaign';
65
64
  /** A job's failure when the site has no campaign writer. */ export const AI_CAMPAIGN_UNAVAILABLE_COPY = 'Campaigns are not available on this site.';
66
65
  /** A job's failure when a campaign whose email was written could not be drafted. */ export const AI_CAMPAIGN_SAVE_FAILURE_COPY = 'The email was written but the campaign could not be drafted. Try the job again.';
@@ -122,12 +121,10 @@ import { registerAiJobStep } from "./ai-jobs.js";
122
121
  drafts: [
123
122
  {
124
123
  resource: AI_EMAIL_DESIGN_RESOURCE,
125
- pluginId: AI_EMAIL_PLUGIN_ID,
126
124
  label: 'Email'
127
125
  },
128
126
  {
129
127
  resource: AI_CAMPAIGN_RESOURCE,
130
- pluginId: AI_CAMPAIGN_PLUGIN_ID,
131
128
  label: 'Marketing'
132
129
  }
133
130
  ],
@@ -207,11 +204,9 @@ export function createAiJobCampaignStep(deps = {}) {
207
204
  hostId,
208
205
  brief: job.brief
209
206
  });
210
- const sendTime = list ? await readAiListSendTime(firestore, {
211
- orgId: job.orgId,
212
- hostId,
207
+ const sendTime = list ? await readAiListSendTime(_extends({}, context, {
213
208
  listId: list.id
214
- }) : null;
209
+ })) : null;
215
210
  const note = aiCampaignAudienceNote(list, sendTime);
216
211
  // A design an earlier run wrote is drafted into its campaign, unspent.
217
212
  if (writtenDesign) {
@@ -233,7 +228,7 @@ export function createAiJobCampaignStep(deps = {}) {
233
228
  readInventory(job.orgId, hostId, {
234
229
  firestore
235
230
  }),
236
- resolveAiEmailProducts(firestore, {
231
+ resolveAiEmailProducts({
237
232
  hostId,
238
233
  brief: job.brief,
239
234
  inputs: job.inputs
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-campaign-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 { checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiModelForStep } from '../providers/routing'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiCampaignAudienceNote,\n readAiListSendTime,\n resolveAiEmailProducts,\n suggestAiCampaignList,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiLimitReview,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n AI_EMAIL_DESIGN_RESOURCE,\n AI_EMAIL_PLUGIN_ID,\n AI_EMAIL_SAVE_FAILURE_COPY,\n AI_JOB_EMAIL_STEP_BUDGETS,\n aiEmailDesignContent,\n aiEmailDesignOutput,\n aiJobInputName,\n generateAiEmail,\n type AiEmailCopy,\n} from './ai-job-email-step'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The campaign step (AGL-2912): one brief becomes an email design AND the\n * draft campaign that would send it.\n *\n * A campaign is an unplanned kind — there is one thing to build and a plan\n * over it would be a plan of one — so the job runs this step alone, and the\n * design it writes is the email step's, generated through `generateAiEmail`\n * so both kinds obey the same doctrine and the same email rules.\n *\n * ## Nothing is sent, and nothing is aimed\n *\n * The campaign is written `draft`, with no audience, no schedule and no\n * sender: a draft campaign is not a queued one, the scheduled processor\n * queries `status == 'scheduled'`, and the send route is the only thing that\n * mails a campaign. Choosing WHO receives it stays a person's decision, so\n * the model is never shown the org's lists, its contacts or what past sends\n * did. Code reads the list the brief names and the send time that list's own\n * history suggests, AFTER the answer, and both reach the person as the\n * output's `note` — never the prompt.\n *\n * ## Where campaign email begins\n *\n * A site whose plan sends no campaign email can still generate email designs;\n * the campaign is the part that needs somewhere to send. So the step gates on\n * the same entitlement the composer and the send route read, and stops the\n * job for a person rather than failing it. The refusal names no plan, because\n * the billing page is the one place that does.\n *\n * ## Run again and it finds its drafts\n *\n * Each write is keyed by the id the job recorded for its draft when it was\n * created, so a run cut off between them finds what it wrote: two drafts are\n * reported, a design alone is drafted into a campaign from the copy the\n * design already stores, and neither spends.\n */\n\n/** The id the marketing plugin is registered under. */\nexport const AI_CAMPAIGN_PLUGIN_ID = 'marketing'\n\n/** The resource the marketing plugin writes a draft campaign under. */\nexport const AI_CAMPAIGN_RESOURCE = 'campaign'\n\n/** A campaign's name when neither the job nor its subject lines give one. */\nexport const AI_JOB_CAMPAIGN_DEFAULT_NAME = 'New campaign'\n\n/** A job's failure when the site has no campaign writer. */\nexport const AI_CAMPAIGN_UNAVAILABLE_COPY = 'Campaigns are not available on this site.'\n\n/** A job's failure when a campaign whose email was written could not be drafted. */\nexport const AI_CAMPAIGN_SAVE_FAILURE_COPY =\n 'The email was written but the campaign could not be drafted. Try the job again.'\n\n/**\n * What a person is told when their plan sends no campaign email. It names the\n * page that sells the answer rather than a plan, so the one place plan names\n * are written stays the one place they are read.\n */\nexport const AI_CAMPAIGN_PLAN_REFUSAL =\n 'Campaign email is not included on this workspace’s plan. ' +\n 'Upgrade in Billing to send campaigns, or generate an email design on its own.'\n\n/** Whether this workspace may send campaign email at all: the composer's and the send route's check. */\nexport function aiCampaignPlanAllows(org: object | null): boolean {\n return checkQuota(org as Partial<AglynOrgBilling> | null, 'emailSendsPerMonth', 0).allowed\n}\n\n/** The campaign as the job's output, carrying what the person still decides. */\nexport function aiCampaignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'campaign',\n id: record.id,\n versionId: null,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/** The subject lines and preheaders a written design already stores. */\nexport function aiCampaignCopyFromDesign(record: PluginDraftRecord): AiEmailCopy {\n const lines = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n const subject = String(record.facts?.['subject'] ?? '')\n const preheader = String(record.facts?.['preheader'] ?? '')\n const subjects = lines(record.facts?.['subjectVariants'])\n const preheaders = lines(record.facts?.['preheaderVariants'])\n return {\n subjects: subjects.length ? subjects : subject ? [subject] : [],\n preheaders: preheaders.length ? preheaders : preheader ? [preheader] : [],\n }\n}\n\n/** The campaign's content: the design it sends, and the copy the send fills its headers from. */\nexport function aiCampaignDraftContent(\n designId: string,\n copy: AiEmailCopy,\n): Record<string, unknown> {\n return {\n templateScreenId: designId,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** A campaign job's name: what the member asked for, else its strongest subject line. */\nexport function aiCampaignName(job: Pick<AiJob, 'inputs'>, copy: AiEmailCopy): string {\n return aiJobInputName(job) || copy.subjects[0] || AI_JOB_CAMPAIGN_DEFAULT_NAME\n}\n\n/**\n * A campaign job is admitted for a site of its org with email and marketing\n * on, room for a design and a campaign, and a plan that sends campaign email.\n */\nexport const aiCampaignJobAdmission: AiJobAdmission = (context) =>\n aiPluginDraftAdmissionRefusal(context, {\n kind: 'campaign',\n drafts: [\n { resource: AI_EMAIL_DESIGN_RESOURCE, pluginId: AI_EMAIL_PLUGIN_ID, label: 'Email' },\n { resource: AI_CAMPAIGN_RESOURCE, pluginId: AI_CAMPAIGN_PLUGIN_ID, label: 'Marketing' },\n ],\n ownCheck: async () =>\n aiCampaignPlanAllows(context.org)\n ? null\n : { status: 403, error: AI_CAMPAIGN_PLAN_REFUSAL },\n })\n\nexport interface AiJobCampaignStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\n/** Both drafts, or the refusal that stands in for a writer the site has not got. */\nfunction writers(\n writerFor: AiPluginDraftWriterLookup,\n): { design: PluginResourceDraftWriter; campaign: PluginResourceDraftWriter } | string {\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n const campaign = writerFor(AI_CAMPAIGN_RESOURCE)\n if (!design) return AI_CAMPAIGN_UNAVAILABLE_COPY\n if (!campaign) return AI_CAMPAIGN_UNAVAILABLE_COPY\n return { design, campaign }\n}\n\nexport function createAiJobCampaignStep(deps: AiJobCampaignStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a campaign job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.campaign') ?? aiModelForStep('job.campaign')\n const found = writers(writerFor)\n if (typeof found === 'string') return { ...aiUnspentOutcome(model), failure: found }\n const { design, campaign } = found\n\n // What an earlier run of this same job already wrote, under the ids the job recorded.\n const designId = aiJobDraftId(job, 'email')\n const [writtenDesign, writtenCampaign, hostSubdomain] = await Promise.all([\n design.read({ hostId, id: designId }),\n campaign.read({ hostId, id: aiJobDraftId(job, 'campaign') }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const place = { hostId, hostSubdomain }\n if (writtenDesign && writtenCampaign) {\n return aiUnspentOutcome(model, {\n outputs: [\n aiEmailDesignOutput(writtenDesign, place, null),\n aiCampaignOutput(writtenCampaign, place, null),\n ],\n })\n }\n\n const orgSnapshot = await firestore.collection('orgs').doc(job.orgId).get()\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n // Asked again here because a plan can change between admission and the run.\n if (!aiCampaignPlanAllows(org)) {\n return aiUnspentOutcome(model, { review: aiLimitReview(AI_CAMPAIGN_PLAN_REFUSAL) })\n }\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n\n // Who it is for and when to send it: read from the site, told to the\n // person, and never sent to the model.\n const list = await suggestAiCampaignList({ hostId, brief: job.brief })\n const sendTime = list ? await readAiListSendTime(firestore, { orgId: job.orgId, hostId, listId: list.id }) : null\n const note = aiCampaignAudienceNote(list, sendTime)\n\n // A design an earlier run wrote is drafted into its campaign, unspent.\n if (writtenDesign) {\n return draftCampaign({\n campaign,\n context,\n job,\n copy: aiCampaignCopyFromDesign(writtenDesign),\n designId: writtenDesign.id,\n spent: aiUnspentOutcome(model),\n outputs: [aiEmailDesignOutput(writtenDesign, place, null)],\n place,\n note,\n })\n }\n\n const [inventory, products] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n resolveAiEmailProducts(firestore, { hostId, brief: job.brief, inputs: job.inputs }),\n ])\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const asked = aiJobInputName(job)\n const generated = await generateAiEmail({\n job,\n step: 'job.campaign',\n model,\n heading: asked ? `Campaign name: ${asked}` : null,\n plan: aiConfirmedPlan(job),\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const { copy } = generated\n const name = aiCampaignName(job, copy)\n const write = await design.write({\n ...context,\n id: designId,\n name,\n content: aiEmailDesignContent(generated.nodes, copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return draftCampaign({\n campaign,\n context,\n job,\n copy,\n designId: write.id,\n spent,\n outputs: [aiEmailDesignOutput(write, place, null)],\n place,\n note,\n })\n }\n}\n\ninterface AiCampaignDraftInput {\n campaign: PluginResourceDraftWriter\n context: { orgId: string; hostId: string; uid: string; org: Record<string, unknown> | null; now: Date }\n job: AiJob\n copy: AiEmailCopy\n designId: string\n /** What the generation cost, or an unspent outcome for a run that reached no model. */\n spent: Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n /** The design's output, reported beside the campaign once it is drafted. */\n outputs: AiJobOutput[]\n place: { hostId: string; hostSubdomain: string | null }\n note: string\n}\n\n/**\n * Drafts the campaign around a design that exists. A campaign that cannot be\n * drafted reports NO outputs: the design on its own is not what the job was\n * asked for, and a run that names it would read as finished.\n */\nasync function draftCampaign(input: AiCampaignDraftInput): Promise<AiJobStepOutcome> {\n const write = await input.campaign.write({\n ...input.context,\n id: aiJobDraftId(input.job, 'campaign'),\n name: aiCampaignName(input.job, input.copy),\n content: aiCampaignDraftContent(input.designId, input.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...input.spent, review: aiLimitReview(write.error) }\n : { ...input.spent, failure: AI_CAMPAIGN_SAVE_FAILURE_COPY }\n }\n return {\n ...input.spent,\n outputs: [...input.outputs, aiCampaignOutput(write, input.place, input.note)],\n }\n}\n\nexport const runAiJobCampaignStep = createAiJobCampaignStep()\n\n/**\n * The least time one campaign step needs before it starts (AGL-3035): the\n * email generation it drafts the campaign's design with, on the campaign's own\n * routing row. Writing the draft campaign asks no model.\n */\nexport const AI_JOB_CAMPAIGN_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.campaign'].minimumMs\n\n/** Registers the campaign step and the check a campaign job passes before it is created or resumed. */\nexport function registerAiCampaignJob(): void {\n registerAiJobStep('campaign', runAiJobCampaignStep, { minimumMs: AI_JOB_CAMPAIGN_STEP_MINIMUM_MS })\n registerAiJobAdmission('campaign', aiCampaignJobAdmission)\n}\n"],"names":["checkQuota","aiModelForStep","readSiteInventory","aiCampaignAudienceNote","readAiListSendTime","resolveAiEmailProducts","suggestAiCampaignList","registerAiJobAdmission","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiLimitReview","aiUnspentOutcome","AI_EMAIL_DESIGN_RESOURCE","AI_EMAIL_PLUGIN_ID","AI_EMAIL_SAVE_FAILURE_COPY","AI_JOB_EMAIL_STEP_BUDGETS","aiEmailDesignContent","aiEmailDesignOutput","aiJobInputName","generateAiEmail","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_CAMPAIGN_PLUGIN_ID","AI_CAMPAIGN_RESOURCE","AI_JOB_CAMPAIGN_DEFAULT_NAME","AI_CAMPAIGN_UNAVAILABLE_COPY","AI_CAMPAIGN_SAVE_FAILURE_COPY","AI_CAMPAIGN_PLAN_REFUSAL","aiCampaignPlanAllows","org","allowed","aiCampaignOutput","record","place","note","resource","id","versionId","hostId","hostSubdomain","label","name","aiCampaignCopyFromDesign","lines","value","Array","isArray","filter","entry","subject","String","facts","preheader","subjects","preheaders","length","aiCampaignDraftContent","designId","copy","templateScreenId","subjectVariants","preheaderVariants","aiCampaignName","job","aiCampaignJobAdmission","context","kind","drafts","pluginId","ownCheck","status","error","writers","writerFor","design","campaign","createAiJobCampaignStep","deps","readInventory","now","signal","firestore","modelFor","orgSnapshot","Error","model","found","failure","writtenDesign","writtenCampaign","Promise","all","read","outputs","collection","doc","orgId","get","data","review","uid","createdBy","list","brief","sendTime","listId","draftCampaign","spent","inventory","products","inputs","refusal","asked","generated","step","heading","plan","result","refused","nodes","write","content","ok","input","runAiJobCampaignStep","AI_JOB_CAMPAIGN_STEP_MINIMUM_MS","minimumMs","registerAiCampaignJob"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-campaign-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 { checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiModelForStep } from '../providers/routing'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiCampaignAudienceNote,\n readAiListSendTime,\n resolveAiEmailProducts,\n suggestAiCampaignList,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiLimitReview,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n AI_EMAIL_DESIGN_RESOURCE,\n AI_EMAIL_SAVE_FAILURE_COPY,\n AI_JOB_EMAIL_STEP_BUDGETS,\n aiEmailDesignContent,\n aiEmailDesignOutput,\n aiJobInputName,\n generateAiEmail,\n type AiEmailCopy,\n} from './ai-job-email-step'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The campaign step (AGL-2912): one brief becomes an email design AND the\n * draft campaign that would send it.\n *\n * A campaign is an unplanned kind — there is one thing to build and a plan\n * over it would be a plan of one — so the job runs this step alone, and the\n * design it writes is the email step's, generated through `generateAiEmail`\n * so both kinds obey the same doctrine and the same email rules.\n *\n * ## Nothing is sent, and nothing is aimed\n *\n * The campaign is written `draft`, with no audience, no schedule and no\n * sender: a draft campaign is not a queued one, the scheduled processor\n * queries `status == 'scheduled'`, and the send route is the only thing that\n * mails a campaign. Choosing WHO receives it stays a person's decision, so\n * the model is never shown the org's lists, its contacts or what past sends\n * did. Code reads the list the brief names and the send time that list's own\n * history suggests, AFTER the answer, and both reach the person as the\n * output's `note` — never the prompt.\n *\n * ## Where campaign email begins\n *\n * A site whose plan sends no campaign email can still generate email designs;\n * the campaign is the part that needs somewhere to send. So the step gates on\n * the same entitlement the composer and the send route read, and stops the\n * job for a person rather than failing it. The refusal names no plan, because\n * the billing page is the one place that does.\n *\n * ## Run again and it finds its drafts\n *\n * Each write is keyed by the id the job recorded for its draft when it was\n * created, so a run cut off between them finds what it wrote: two drafts are\n * reported, a design alone is drafted into a campaign from the copy the\n * design already stores, and neither spends.\n */\n\n/** The resource the marketing plugin writes a draft campaign under. */\nexport const AI_CAMPAIGN_RESOURCE = 'campaign'\n\n/** A campaign's name when neither the job nor its subject lines give one. */\nexport const AI_JOB_CAMPAIGN_DEFAULT_NAME = 'New campaign'\n\n/** A job's failure when the site has no campaign writer. */\nexport const AI_CAMPAIGN_UNAVAILABLE_COPY = 'Campaigns are not available on this site.'\n\n/** A job's failure when a campaign whose email was written could not be drafted. */\nexport const AI_CAMPAIGN_SAVE_FAILURE_COPY =\n 'The email was written but the campaign could not be drafted. Try the job again.'\n\n/**\n * What a person is told when their plan sends no campaign email. It names the\n * page that sells the answer rather than a plan, so the one place plan names\n * are written stays the one place they are read.\n */\nexport const AI_CAMPAIGN_PLAN_REFUSAL =\n 'Campaign email is not included on this workspace’s plan. ' +\n 'Upgrade in Billing to send campaigns, or generate an email design on its own.'\n\n/** Whether this workspace may send campaign email at all: the composer's and the send route's check. */\nexport function aiCampaignPlanAllows(org: object | null): boolean {\n return checkQuota(org as Partial<AglynOrgBilling> | null, 'emailSendsPerMonth', 0).allowed\n}\n\n/** The campaign as the job's output, carrying what the person still decides. */\nexport function aiCampaignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'campaign',\n id: record.id,\n versionId: null,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/** The subject lines and preheaders a written design already stores. */\nexport function aiCampaignCopyFromDesign(record: PluginDraftRecord): AiEmailCopy {\n const lines = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n const subject = String(record.facts?.['subject'] ?? '')\n const preheader = String(record.facts?.['preheader'] ?? '')\n const subjects = lines(record.facts?.['subjectVariants'])\n const preheaders = lines(record.facts?.['preheaderVariants'])\n return {\n subjects: subjects.length ? subjects : subject ? [subject] : [],\n preheaders: preheaders.length ? preheaders : preheader ? [preheader] : [],\n }\n}\n\n/** The campaign's content: the design it sends, and the copy the send fills its headers from. */\nexport function aiCampaignDraftContent(\n designId: string,\n copy: AiEmailCopy,\n): Record<string, unknown> {\n return {\n templateScreenId: designId,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** A campaign job's name: what the member asked for, else its strongest subject line. */\nexport function aiCampaignName(job: Pick<AiJob, 'inputs'>, copy: AiEmailCopy): string {\n return aiJobInputName(job) || copy.subjects[0] || AI_JOB_CAMPAIGN_DEFAULT_NAME\n}\n\n/**\n * A campaign job is admitted for a site of its org with email and marketing\n * on, room for a design and a campaign, and a plan that sends campaign email.\n */\nexport const aiCampaignJobAdmission: AiJobAdmission = (context) =>\n aiPluginDraftAdmissionRefusal(context, {\n kind: 'campaign',\n drafts: [\n { resource: AI_EMAIL_DESIGN_RESOURCE, label: 'Email' },\n { resource: AI_CAMPAIGN_RESOURCE, label: 'Marketing' },\n ],\n ownCheck: async () =>\n aiCampaignPlanAllows(context.org)\n ? null\n : { status: 403, error: AI_CAMPAIGN_PLAN_REFUSAL },\n })\n\nexport interface AiJobCampaignStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\n/** Both drafts, or the refusal that stands in for a writer the site has not got. */\nfunction writers(\n writerFor: AiPluginDraftWriterLookup,\n): { design: PluginResourceDraftWriter; campaign: PluginResourceDraftWriter } | string {\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n const campaign = writerFor(AI_CAMPAIGN_RESOURCE)\n if (!design) return AI_CAMPAIGN_UNAVAILABLE_COPY\n if (!campaign) return AI_CAMPAIGN_UNAVAILABLE_COPY\n return { design, campaign }\n}\n\nexport function createAiJobCampaignStep(deps: AiJobCampaignStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a campaign job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.campaign') ?? aiModelForStep('job.campaign')\n const found = writers(writerFor)\n if (typeof found === 'string') return { ...aiUnspentOutcome(model), failure: found }\n const { design, campaign } = found\n\n // What an earlier run of this same job already wrote, under the ids the job recorded.\n const designId = aiJobDraftId(job, 'email')\n const [writtenDesign, writtenCampaign, hostSubdomain] = await Promise.all([\n design.read({ hostId, id: designId }),\n campaign.read({ hostId, id: aiJobDraftId(job, 'campaign') }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const place = { hostId, hostSubdomain }\n if (writtenDesign && writtenCampaign) {\n return aiUnspentOutcome(model, {\n outputs: [\n aiEmailDesignOutput(writtenDesign, place, null),\n aiCampaignOutput(writtenCampaign, place, null),\n ],\n })\n }\n\n const orgSnapshot = await firestore.collection('orgs').doc(job.orgId).get()\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n // Asked again here because a plan can change between admission and the run.\n if (!aiCampaignPlanAllows(org)) {\n return aiUnspentOutcome(model, { review: aiLimitReview(AI_CAMPAIGN_PLAN_REFUSAL) })\n }\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n\n // Who it is for and when to send it: read from the site, told to the\n // person, and never sent to the model.\n const list = await suggestAiCampaignList({ hostId, brief: job.brief })\n const sendTime = list ? await readAiListSendTime({ ...context, listId: list.id }) : null\n const note = aiCampaignAudienceNote(list, sendTime)\n\n // A design an earlier run wrote is drafted into its campaign, unspent.\n if (writtenDesign) {\n return draftCampaign({\n campaign,\n context,\n job,\n copy: aiCampaignCopyFromDesign(writtenDesign),\n designId: writtenDesign.id,\n spent: aiUnspentOutcome(model),\n outputs: [aiEmailDesignOutput(writtenDesign, place, null)],\n place,\n note,\n })\n }\n\n const [inventory, products] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n resolveAiEmailProducts({ hostId, brief: job.brief, inputs: job.inputs }),\n ])\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const asked = aiJobInputName(job)\n const generated = await generateAiEmail({\n job,\n step: 'job.campaign',\n model,\n heading: asked ? `Campaign name: ${asked}` : null,\n plan: aiConfirmedPlan(job),\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const { copy } = generated\n const name = aiCampaignName(job, copy)\n const write = await design.write({\n ...context,\n id: designId,\n name,\n content: aiEmailDesignContent(generated.nodes, copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return draftCampaign({\n campaign,\n context,\n job,\n copy,\n designId: write.id,\n spent,\n outputs: [aiEmailDesignOutput(write, place, null)],\n place,\n note,\n })\n }\n}\n\ninterface AiCampaignDraftInput {\n campaign: PluginResourceDraftWriter\n context: { orgId: string; hostId: string; uid: string; org: Record<string, unknown> | null; now: Date }\n job: AiJob\n copy: AiEmailCopy\n designId: string\n /** What the generation cost, or an unspent outcome for a run that reached no model. */\n spent: Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n /** The design's output, reported beside the campaign once it is drafted. */\n outputs: AiJobOutput[]\n place: { hostId: string; hostSubdomain: string | null }\n note: string\n}\n\n/**\n * Drafts the campaign around a design that exists. A campaign that cannot be\n * drafted reports NO outputs: the design on its own is not what the job was\n * asked for, and a run that names it would read as finished.\n */\nasync function draftCampaign(input: AiCampaignDraftInput): Promise<AiJobStepOutcome> {\n const write = await input.campaign.write({\n ...input.context,\n id: aiJobDraftId(input.job, 'campaign'),\n name: aiCampaignName(input.job, input.copy),\n content: aiCampaignDraftContent(input.designId, input.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...input.spent, review: aiLimitReview(write.error) }\n : { ...input.spent, failure: AI_CAMPAIGN_SAVE_FAILURE_COPY }\n }\n return {\n ...input.spent,\n outputs: [...input.outputs, aiCampaignOutput(write, input.place, input.note)],\n }\n}\n\nexport const runAiJobCampaignStep = createAiJobCampaignStep()\n\n/**\n * The least time one campaign step needs before it starts (AGL-3035): the\n * email generation it drafts the campaign's design with, on the campaign's own\n * routing row. Writing the draft campaign asks no model.\n */\nexport const AI_JOB_CAMPAIGN_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.campaign'].minimumMs\n\n/** Registers the campaign step and the check a campaign job passes before it is created or resumed. */\nexport function registerAiCampaignJob(): void {\n registerAiJobStep('campaign', runAiJobCampaignStep, { minimumMs: AI_JOB_CAMPAIGN_STEP_MINIMUM_MS })\n registerAiJobAdmission('campaign', aiCampaignJobAdmission)\n}\n"],"names":["checkQuota","aiModelForStep","readSiteInventory","aiCampaignAudienceNote","readAiListSendTime","resolveAiEmailProducts","suggestAiCampaignList","registerAiJobAdmission","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiLimitReview","aiUnspentOutcome","AI_EMAIL_DESIGN_RESOURCE","AI_EMAIL_SAVE_FAILURE_COPY","AI_JOB_EMAIL_STEP_BUDGETS","aiEmailDesignContent","aiEmailDesignOutput","aiJobInputName","generateAiEmail","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_CAMPAIGN_RESOURCE","AI_JOB_CAMPAIGN_DEFAULT_NAME","AI_CAMPAIGN_UNAVAILABLE_COPY","AI_CAMPAIGN_SAVE_FAILURE_COPY","AI_CAMPAIGN_PLAN_REFUSAL","aiCampaignPlanAllows","org","allowed","aiCampaignOutput","record","place","note","resource","id","versionId","hostId","hostSubdomain","label","name","aiCampaignCopyFromDesign","lines","value","Array","isArray","filter","entry","subject","String","facts","preheader","subjects","preheaders","length","aiCampaignDraftContent","designId","copy","templateScreenId","subjectVariants","preheaderVariants","aiCampaignName","job","aiCampaignJobAdmission","context","kind","drafts","ownCheck","status","error","writers","writerFor","design","campaign","createAiJobCampaignStep","deps","readInventory","now","signal","firestore","modelFor","orgSnapshot","Error","model","found","failure","writtenDesign","writtenCampaign","Promise","all","read","outputs","collection","doc","orgId","get","data","review","uid","createdBy","list","brief","sendTime","listId","draftCampaign","spent","inventory","products","inputs","refusal","asked","generated","step","heading","plan","result","refused","nodes","write","content","ok","input","runAiJobCampaignStep","AI_JOB_CAMPAIGN_STEP_MINIMUM_MS","minimumMs","registerAiCampaignJob"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,2CAA0C;AAOrE,SAASC,cAAc,QAAQ,0BAAsB;AACrD,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,sBAAsB,EACtBC,qBAAqB,QAChB,yBAAqB;AAC5B,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SAASC,YAAY,QAAQ,wBAAoB;AACjD,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SACEC,eAAe,EACfC,gBAAgB,EAChBC,iBAAiB,EACjBC,aAAa,EACbC,gBAAgB,QACX,yBAAqB;AAC5B,SACEC,wBAAwB,EACxBC,0BAA0B,EAC1BC,yBAAyB,EACzBC,oBAAoB,EACpBC,mBAAmB,EACnBC,cAAc,EACdC,eAAe,QAEV,yBAAqB;AAC5B,SACEC,6BAA6B,EAC7BC,mBAAmB,QAEd,4BAAwB;AAE/B,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GAED,qEAAqE,GACrE,OAAO,MAAMC,uBAAuB,WAAU;AAE9C,2EAA2E,GAC3E,OAAO,MAAMC,+BAA+B,eAAc;AAE1D,0DAA0D,GAC1D,OAAO,MAAMC,+BAA+B,4CAA2C;AAEvF,kFAAkF,GAClF,OAAO,MAAMC,gCACX,kFAAiF;AAEnF;;;;CAIC,GACD,OAAO,MAAMC,2BACX,8DACA,gFAA+E;AAEjF,sGAAsG,GACtG,OAAO,SAASC,qBAAqBC,GAAkB;IACrD,OAAO/B,WAAW+B,KAAwC,sBAAsB,GAAGC,OAAO;AAC5F;AAEA,8EAA8E,GAC9E,OAAO,SAASC,iBACdC,MAAyB,EACzBC,KAAuD,EACvDC,IAAmB;IAEnB,OAAO;QACLC,UAAU;QACVC,IAAIJ,OAAOI,EAAE;QACbC,WAAW;QACXC,QAAQL,MAAMK,MAAM;QACpBC,eAAeN,MAAMM,aAAa;QAClCC,OAAOR,OAAOS,IAAI;OACdP,OAAO;QAAEA;IAAK,IAAI,CAAC;AAE3B;AAEA,sEAAsE,GACtE,OAAO,SAASQ,yBAAyBV,MAAyB;;QAGzCA,eACEA,gBACFA,gBACEA;IALzB,MAAMW,QAAQ,CAACC,QACbC,MAAMC,OAAO,CAACF,SAASA,MAAMG,MAAM,CAAC,CAACC,QAA2B,OAAOA,UAAU,YAAY,EAAE;IACjG,MAAMC,UAAUC,gBAAOlB,gBAAAA,OAAOmB,KAAK,qBAAZnB,aAAc,CAAC,UAAU,mBAAI;IACpD,MAAMoB,YAAYF,iBAAOlB,iBAAAA,OAAOmB,KAAK,qBAAZnB,cAAc,CAAC,YAAY,oBAAI;IACxD,MAAMqB,WAAWV,OAAMX,iBAAAA,OAAOmB,KAAK,qBAAZnB,cAAc,CAAC,kBAAkB;IACxD,MAAMsB,aAAaX,OAAMX,iBAAAA,OAAOmB,KAAK,qBAAZnB,cAAc,CAAC,oBAAoB;IAC5D,OAAO;QACLqB,UAAUA,SAASE,MAAM,GAAGF,WAAWJ,UAAU;YAACA;SAAQ,GAAG,EAAE;QAC/DK,YAAYA,WAAWC,MAAM,GAAGD,aAAaF,YAAY;YAACA;SAAU,GAAG,EAAE;IAC3E;AACF;AAEA,+FAA+F,GAC/F,OAAO,SAASI,uBACdC,QAAgB,EAChBC,IAAiB;QAINA,iBACEA;IAHb,OAAO;QACLC,kBAAkBF;QAClBR,OAAO,GAAES,kBAAAA,KAAKL,QAAQ,CAAC,EAAE,YAAhBK,kBAAoB;QAC7BN,SAAS,GAAEM,oBAAAA,KAAKJ,UAAU,CAAC,EAAE,YAAlBI,oBAAsB;QACjCE,iBAAiBF,KAAKL,QAAQ;QAC9BQ,mBAAmBH,KAAKJ,UAAU;IACpC;AACF;AAEA,uFAAuF,GACvF,OAAO,SAASQ,eAAeC,GAA0B,EAAEL,IAAiB;IAC1E,OAAOxC,eAAe6C,QAAQL,KAAKL,QAAQ,CAAC,EAAE,IAAI7B;AACpD;AAEA;;;CAGC,GACD,OAAO,MAAMwC,yBAAyC,CAACC,UACrD7C,8BAA8B6C,SAAS;QACrCC,MAAM;QACNC,QAAQ;YACN;gBAAEhC,UAAUtB;gBAA0B2B,OAAO;YAAQ;YACrD;gBAAEL,UAAUZ;gBAAsBiB,OAAO;YAAY;SACtD;QACD4B,UAAU,UACRxC,qBAAqBqC,QAAQpC,GAAG,IAC5B,OACA;gBAAEwC,QAAQ;gBAAKC,OAAO3C;YAAyB;IACvD,GAAE;AASJ,kFAAkF,GAClF,SAAS4C,QACPC,SAAoC;IAEpC,MAAMC,SAASD,UAAU3D;IACzB,MAAM6D,WAAWF,UAAUjD;IAC3B,IAAI,CAACkD,QAAQ,OAAOhD;IACpB,IAAI,CAACiD,UAAU,OAAOjD;IACtB,OAAO;QAAEgD;QAAQC;IAAS;AAC5B;AAEA,OAAO,SAASC,wBAAwBC,OAA8B,CAAC,CAAC;QAChDA,qBACJA;IADlB,MAAMC,iBAAgBD,sBAAAA,KAAKC,aAAa,YAAlBD,sBAAsB5E;IAC5C,MAAMwE,aAAYI,kBAAAA,KAAKJ,SAAS,YAAdI,kBAAkBvD;IACpC,OAAO,OAAO,EAAE0C,GAAG,EAAEe,GAAG,EAAEC,MAAM,EAAEC,SAAS,EAAEC,QAAQ,EAAE;kBA2BxCC;QA1Bb,MAAM5C,SAASyB,IAAIzB,MAAM;QACzB,IAAI,CAACA,QAAQ,MAAM,IAAI6C,MAAM;QAC7B,yEAAyE;QACzE,MAAMC,gBAAQH,4BAAAA,SAAW,kCAAmBlF,eAAe;QAC3D,MAAMsF,QAAQd,QAAQC;QACtB,IAAI,OAAOa,UAAU,UAAU,OAAO,aAAKzE,iBAAiBwE;YAAQE,SAASD;;QAC7E,MAAM,EAAEZ,MAAM,EAAEC,QAAQ,EAAE,GAAGW;QAE7B,sFAAsF;QACtF,MAAM5B,WAAWnD,aAAayD,KAAK;QACnC,MAAM,CAACwB,eAAeC,iBAAiBjD,cAAc,GAAG,MAAMkD,QAAQC,GAAG,CAAC;YACxEjB,OAAOkB,IAAI,CAAC;gBAAErD;gBAAQF,IAAIqB;YAAS;YACnCiB,SAASiB,IAAI,CAAC;gBAAErD;gBAAQF,IAAI9B,aAAayD,KAAK;YAAY;YAC1DxD,gBAAgByE,WAAW1C;SAC5B;QACD,MAAML,QAAQ;YAAEK;YAAQC;QAAc;QACtC,IAAIgD,iBAAiBC,iBAAiB;YACpC,OAAO5E,iBAAiBwE,OAAO;gBAC7BQ,SAAS;oBACP3E,oBAAoBsE,eAAetD,OAAO;oBAC1CF,iBAAiByD,iBAAiBvD,OAAO;iBAC1C;YACH;QACF;QAEA,MAAMiD,cAAc,MAAMF,UAAUa,UAAU,CAAC,QAAQC,GAAG,CAAC/B,IAAIgC,KAAK,EAAEC,GAAG;QACzE,MAAMnE,OAAOqD,oBAAAA,YAAYe,IAAI,cAAhBf,oBAAsB;QACnC,4EAA4E;QAC5E,IAAI,CAACtD,qBAAqBC,MAAM;YAC9B,OAAOjB,iBAAiBwE,OAAO;gBAAEc,QAAQvF,cAAcgB;YAA0B;QACnF;QACA,MAAMsC,UAAU;YAAE8B,OAAOhC,IAAIgC,KAAK;YAAEzD;YAAQ6D,KAAKpC,IAAIqC,SAAS;YAAEvE;YAAKiD;QAAI;QAEzE,qEAAqE;QACrE,uCAAuC;QACvC,MAAMuB,OAAO,MAAMjG,sBAAsB;YAAEkC;YAAQgE,OAAOvC,IAAIuC,KAAK;QAAC;QACpE,MAAMC,WAAWF,OAAO,MAAMnG,mBAAmB,aAAK+D;YAASuC,QAAQH,KAAKjE,EAAE;cAAM;QACpF,MAAMF,OAAOjC,uBAAuBoG,MAAME;QAE1C,uEAAuE;QACvE,IAAIhB,eAAe;YACjB,OAAOkB,cAAc;gBACnB/B;gBACAT;gBACAF;gBACAL,MAAMhB,yBAAyB6C;gBAC/B9B,UAAU8B,cAAcnD,EAAE;gBAC1BsE,OAAO9F,iBAAiBwE;gBACxBQ,SAAS;oBAAC3E,oBAAoBsE,eAAetD,OAAO;iBAAM;gBAC1DA;gBACAC;YACF;QACF;QAEA,MAAM,CAACyE,WAAWC,SAAS,GAAG,MAAMnB,QAAQC,GAAG,CAAC;YAC9Cb,cAAcd,IAAIgC,KAAK,EAAEzD,QAAQ;gBAAE0C;YAAU;YAC7C7E,uBAAuB;gBAAEmC;gBAAQgE,OAAOvC,IAAIuC,KAAK;gBAAEO,QAAQ9C,IAAI8C,MAAM;YAAC;SACvE;QACD,MAAMC,UAAU,MAAMrC,OAAOqC,OAAO,CAAC7C;QACrC,IAAI6C,SAAS;YACX,OAAOA,QAAQzC,MAAM,KAAK,MACtBzD,iBAAiBwE,OAAO;gBAAEc,QAAQvF,cAAcmG,QAAQxC,KAAK;YAAE,KAC/D,aAAK1D,iBAAiBwE;gBAAQE,SAASwB,QAAQxC,KAAK;;QAC1D;QAEA,MAAMyC,QAAQ7F,eAAe6C;QAC7B,MAAMiD,YAAY,MAAM7F,gBAAgB;YACtC4C;YACAkD,MAAM;YACN7B;YACA8B,SAASH,QAAQ,CAAC,eAAe,EAAEA,OAAO,GAAG;YAC7CI,MAAM3G,gBAAgBuD;YACtB4C;YACArE;YACAsE;YACAnC;WACIM,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAM,EAAEqC,MAAM,EAAE,GAAGJ;QACnB,MAAMN,QAAQhG,kBAAkB0G;QAChC,IAAIA,OAAO/C,MAAM,KAAK,WAAW,OAAO,aAAKqC;YAAOW,SAAS;;QAC7D,IAAID,OAAO/C,MAAM,KAAK,eAAe,OAAO,aAAKqC;YAAOR,QAAQzF,iBAAiB2G;;QACjF,IAAI,CAACJ,UAAUtD,IAAI,IAAI,CAACsD,UAAUM,KAAK,EAAE,OAAO,aAAKZ;YAAOpB,SAASxE;;QAErE,MAAM,EAAE4C,IAAI,EAAE,GAAGsD;QACjB,MAAMvE,OAAOqB,eAAeC,KAAKL;QACjC,MAAM6D,QAAQ,MAAM9C,OAAO8C,KAAK,CAAC,aAC5BtD;YACH7B,IAAIqB;YACJhB;YACA+E,SAASxG,qBAAqBgG,UAAUM,KAAK,EAAE5D;;QAEjD,IAAI6D,MAAME,EAAE,KAAK,OAAO;YACtB,OAAOF,MAAMlD,MAAM,KAAK,MACpB,aAAKqC;gBAAOR,QAAQvF,cAAc4G,MAAMjD,KAAK;iBAC7C,aAAKoC;gBAAOpB,SAASxE;;QAC3B;QACA,OAAO2F,cAAc;YACnB/B;YACAT;YACAF;YACAL;YACAD,UAAU8D,MAAMnF,EAAE;YAClBsE;YACAd,SAAS;gBAAC3E,oBAAoBsG,OAAOtF,OAAO;aAAM;YAClDA;YACAC;QACF;IACF;AACF;AAgBA;;;;CAIC,GACD,eAAeuE,cAAciB,KAA2B;IACtD,MAAMH,QAAQ,MAAMG,MAAMhD,QAAQ,CAAC6C,KAAK,CAAC,aACpCG,MAAMzD,OAAO;QAChB7B,IAAI9B,aAAaoH,MAAM3D,GAAG,EAAE;QAC5BtB,MAAMqB,eAAe4D,MAAM3D,GAAG,EAAE2D,MAAMhE,IAAI;QAC1C8D,SAAShE,uBAAuBkE,MAAMjE,QAAQ,EAAEiE,MAAMhE,IAAI;;IAE5D,IAAI6D,MAAME,EAAE,KAAK,OAAO;QACtB,OAAOF,MAAMlD,MAAM,KAAK,MACpB,aAAKqD,MAAMhB,KAAK;YAAER,QAAQvF,cAAc4G,MAAMjD,KAAK;aACnD,aAAKoD,MAAMhB,KAAK;YAAEpB,SAAS5D;;IACjC;IACA,OAAO,aACFgG,MAAMhB,KAAK;QACdd,SAAS;eAAI8B,MAAM9B,OAAO;YAAE7D,iBAAiBwF,OAAOG,MAAMzF,KAAK,EAAEyF,MAAMxF,IAAI;SAAE;;AAEjF;AAEA,OAAO,MAAMyF,uBAAuBhD,0BAAyB;AAE7D;;;;CAIC,GACD,OAAO,MAAMiD,kCAAkC7G,yBAAyB,CAAC,eAAe,CAAC8G,SAAS,CAAA;AAElG,qGAAqG,GACrG,OAAO,SAASC;IACdxG,kBAAkB,YAAYqG,sBAAsB;QAAEE,WAAWD;IAAgC;IACjGvH,uBAAuB,YAAY2D;AACrC"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { duplicateResource } from "@aglyn/tenant-data-admin/server/duplicate-resource";
18
+ import { aiPlanEmbedsFor } from "../model/ai-build-plan.js";
18
19
  import { AI_STEP_TIERS } from "../providers/catalog.js";
19
20
  import { AI_ROUTING_TABLE, aiModelForStep } from "../providers/routing.js";
20
21
  import { runValidatedGeneration } from "../runtime/ai-doctrine.js";
@@ -77,7 +78,7 @@ import { registerAiJobStep } from "./ai-jobs.js";
77
78
  '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',
78
79
  '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',
79
80
  'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled "Hide <the part>" whose default is "false", and the part’s own element carries "hideIf": "{{prop.<name>}}". Never hide the whole component.',
80
- 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or ""; for image "", for an upload; for icon "", for the site owner to pick; for choice one of its answers’ values.'
81
+ 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a page id from the site inventory, or ""; for image "", for an upload; for icon "", for the site owner to pick; for choice one of its answers’ values.'
81
82
  ].join('\n')
82
83
  }
83
84
  ];
@@ -262,9 +263,14 @@ export function createAiJobComponentStep(deps = {}) {
262
263
  } : {}, AI_ROUTING_TABLE['job.component'].effort ? {
263
264
  effort: AI_ROUTING_TABLE['job.component'].effort
264
265
  } : {}, {
265
- context: {
266
+ // A third-party player the confirmed plan lists for this component is the only one it may embed (AGL-3433).
267
+ context: _extends({
266
268
  definesComponent: true
267
- },
269
+ }, creation ? {
270
+ plannedEmbeds: aiPlanEmbedsFor(plan, {
271
+ create: creation.name
272
+ })
273
+ } : {}),
268
274
  extend: aiComponentCheck({
269
275
  inventory,
270
276
  plan,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck, aiPlannedComponentProps } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftComponentProps,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/**\n * The props the confirmed plan gives the component, against the component a\n * COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or\n * when the plan promised no prop to keep.\n *\n * The third kind to need this, after the layout (`aiCopiedLayoutReview`) and\n * the page (`aiCopiedPageReview`), and found the same way: a duplicate branch\n * generates nothing, so it is the one place a plan's promise can go unmet\n * with no model to answer for it.\n *\n * MEASURED 2026-09-21. A plan read \"the existing card has no link target; the\n * brief needs one\", and `practice-area-link-card` came out BYTE-IDENTICAL to\n * `practice-area-card` — the same seven nodes under the same ids, the same\n * three props, no link anywhere. Its job reported the copy as built. So did\n * `practice-area-link-card-v2` the following pass. A component job can\n * therefore charge a member for a duplicate of something they already had.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedComponentReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = aiPlannedComponentProps(input.plan)\n if (!promised.length) return null\n const declared = await readAiDraftComponentProps(firestore, { hostId: input.hostId, id: input.id })\n if (declared === null) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,\n findings: [],\n }\n }\n const missing = promised.filter((name) => !declared.includes(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a property' : 'properties'\n } the copy does not have.`,\n violations: [\n {\n rule: null,\n code: 'plan-props-missing',\n message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${\n declared.length ? declared.join(', ') : 'none of them'\n }. Add ${one ? 'it' : 'each of them'}, and use ${\n one ? 'it' : 'them'\n } in the component, or the copy is the component it was copied from.`,\n },\n ],\n })\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so without this a plan\n // reading \"duplicate it and add a link\" reports Done on a duplicate\n // that added nothing.\n const review = await aiCopiedComponentReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n context: { definesComponent: true },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiPlannedComponentProps","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftComponentProps","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiCopiedComponentReview","firestore","input","promised","length","declared","hostId","id","reason","message","findings","missing","filter","includes","one","violations","rule","code","aiComponentJobAdmission","context","orgId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","extend","onProps","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID,SAASA,iBAAiB,QAAQ,qDAAoD;AAEtF,SAASC,aAAa,QAAQ,0BAAsB;AACpD,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AACvE,SAASC,sBAAsB,QAAQ,4BAAwB;AAI/D,SAASC,iBAAiB,QAAQ,+BAA2B;AAC7D,SACEC,sBAAsB,EACtBC,wBAAwB,EACxBC,eAAe,QACV,gCAA4B;AACnC,SAASC,sBAAsB,QAA6B,wBAAoB;AAChF,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,gBAAgB,EAAEC,uBAAuB,QAAQ,+BAA2B;AACrF,SAASC,YAAY,QAAQ,wBAAoB;AACjD,SACEC,uBAAuB,EACvBC,uBAAuB,EACvBC,eAAe,EACfC,WAAW,EACXC,yBAAyB,EACzBC,YAAY,QAEP,qBAAiB;AACxB,SACEC,oBAAoB,EACpBC,eAAe,EACfC,gBAAgB,EAChBC,iBAAiB,EACjBC,cAAc,EACdC,aAAa,EACbC,cAAc,EACdC,oBAAoB,EACpBC,gBAAgB,QACX,yBAAqB;AAE5B,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,oGAAoG,GACpG,OAAO,MAAMC,8BAA8B5B,gBAAgB,CAAC,gBAAgB,CAAC6B,SAAS,CAAA;AAEtF;;;;CAIC,GACD,OAAO,MAAMC,+BAA+BtB,gBAAgB;IAC1DuB,MAAMhC,aAAa,CAAC,gBAAgB;IACpC8B,WAAWD;AACb,GAAE;AAEF,8DAA8D,GAC9D,OAAO,MAAMI,mCAAmCF,6BAA6BG,SAAS,CAAA;AAEtF,iDAAiD,GACjD,OAAO,MAAMC,gCAAgC,YAAW;AAExD,6FAA6F,GAC7F,OAAO,MAAMC,gCAA0D;IACrE;QACEC,MAAM;YACJ,CAAC,6IAA6I,EAAEhC,uBAAuB,4EAA4E,CAAC;YACpP;YACA;YACA,CAAC,iDAAiD,EAAEC,2BAA2B,CAAC,CAAC;YACjF;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD,CAACgC,IAAI,CAAC;IACT;CACD,CAAA;AAED,8FAA8F,GAC9F,OAAO,SAASC,qBACdC,GAAyB,EACzBC,IAAsB,EACtBC,IAAY;IAEZ,OAAO;QAAC,CAAC,gBAAgB,EAAEA,MAAM;QAAEnB,eAAeiB;WAASd,qBAAqBe;KAAM,CAACH,IAAI,CAAC;AAC9F;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,eAAeK,wBACpBC,SAAsC,EACtCC,KAA2E;IAE3E,MAAMC,WAAWnC,wBAAwBkC,MAAMJ,IAAI;IACnD,IAAI,CAACK,SAASC,MAAM,EAAE,OAAO;IAC7B,MAAMC,WAAW,MAAM/B,0BAA0B2B,WAAW;QAAEK,QAAQJ,MAAMI,MAAM;QAAEC,IAAIL,MAAMK,EAAE;IAAC;IACjG,IAAIF,aAAa,MAAM;QACrB,OAAO;YACLG,QAAQ;YACRC,SAAS,CAAC,CAAC,EAAEP,MAAMH,IAAI,CAAC,gMAAgM,CAAC;YACzNW,UAAU,EAAE;QACd;IACF;IACA,MAAMC,UAAUR,SAASS,MAAM,CAAC,CAACb,OAAS,CAACM,SAASQ,QAAQ,CAACd;IAC7D,IAAI,CAACY,QAAQP,MAAM,EAAE,OAAO;IAC5B,MAAMU,MAAMH,QAAQP,MAAM,KAAK;IAC/B,OAAO1B,iBAAiB;QACtB+B,SAAS,CAAC,CAAC,EAAEP,MAAMH,IAAI,CAAC,wFAAwF,EAC9Ge,MAAM,eAAe,aACtB,wBAAwB,CAAC;QAC1BC,YAAY;YACV;gBACEC,MAAM;gBACNC,MAAM;gBACNR,SAAS,CAAC,2CAA2C,EAAEE,QAAQhB,IAAI,CAAC,MAAM,mBAAmB,EAC3FU,SAASD,MAAM,GAAGC,SAASV,IAAI,CAAC,QAAQ,eACzC,MAAM,EAAEmB,MAAM,OAAO,eAAe,UAAU,EAC7CA,MAAM,OAAO,OACd,mEAAmE,CAAC;YACvE;SACD;IACH;AACF;AAEA,yGAAyG,GACzG,OAAO,MAAMI,0BAA0C,CAACC,UACtDjD,wBAAwBiD,QAAQlB,SAAS,EAAE;QACzCmB,OAAOD,QAAQC,KAAK;QACpBd,QAAQa,QAAQb,MAAM;QACtBe,MAAM;QACNC,KAAKH,QAAQG,GAAG;IAClB,GAAE;AASJ,OAAO,SAASC,yBAAyBC,OAA+B,CAAC,CAAC;QAClDA,qBACJA;IADlB,MAAMC,iBAAgBD,sBAAAA,KAAKC,aAAa,YAAlBD,sBAAsB/D;IAC5C,MAAMiE,aAAYF,kBAAAA,KAAKE,SAAS,YAAdF,kBAAkBpE;IACpC,OAAO,OAAO,EAAEyC,GAAG,EAAE8B,GAAG,EAAEC,MAAM,EAAE3B,SAAS,EAAE4B,QAAQ,EAAE;kBAyBxCC;QAxBb,MAAMxB,SAAST,IAAIS,MAAM;QACzB,IAAI,CAACA,QAAQ,MAAM,IAAIyB,MAAM;QAC7B,MAAMC,SAAS,CAACC,OAAsBC,MAA8BC,OAAuC;gBACzGC,UAAU;gBACV7B,IAAI0B,MAAM1B,EAAE;gBACZ8B,WAAWJ,MAAMI,SAAS;gBAC1B/B;gBACAgC,eAAeL,MAAMK,aAAa;gBAClCC,OAAON,MAAMlC,IAAI;eACbmC,OAAO;gBAAEA;YAAK,IAAI,CAAC,GACnBC,OAAO;gBAAEA;YAAK,IAAI,CAAC;QAEzB,yEAAyE;QACzE,MAAMK,gBAAQX,4BAAAA,SAAW,mCAAoBtE,eAAe;QAE5D,kGAAkG;QAClG,MAAMkF,UAAUxE,aAAa4B,KAAK;QAClC,MAAM6C,UAAU,MAAMrE,YAAY4B,WAAW;YAAEoB,MAAM;YAAaf;YAAQC,IAAIkC;QAAQ;QACtF,IAAIC,SAAS,OAAO1D,iBAAiBwD,OAAO;YAAEG,SAAS;gBAACX,OAAOU;aAAS;QAAC;QAEzE,MAAM,CAACE,WAAWd,YAAY,GAAG,MAAMe,QAAQC,GAAG,CAAC;YACjDrB,cAAc5B,IAAIuB,KAAK,EAAEd,QAAQ;gBAAEL;YAAU;YAC7CA,UAAU8C,UAAU,CAAC,QAAQC,GAAG,CAACnD,IAAIuB,KAAK,EAAE6B,GAAG;SAChD;QACD,MAAM3B,OAAOQ,oBAAAA,YAAYoB,IAAI,cAAhBpB,oBAAsB;QACnC,MAAMhC,OAAOrB,gBAAgBoB;QAC7B,MAAMsD,WAAWrE,eAAegB,MAAM;QACtC,MAAMC,OAAOoD,CAAAA,4BAAAA,SAAUpD,IAAI,KAAIP;QAE/B,yEAAyE;QACzE,uEAAuE;QACvE,iDAAiD;QACjD,IACE2D,CAAAA,4BAAAA,SAAUC,WAAW,KACrBR,UAAUS,UAAU,CAACC,IAAI,CAAC,CAACC,YAAcA,UAAUhD,EAAE,KAAK4C,SAASC,WAAW,GAC9E;YACA,MAAMI,OAAO,MAAM9B,UAAU,aAAa;gBACxCN,OAAOvB,IAAIuB,KAAK;gBAChBd;gBACAmD,UAAUN,SAASC,WAAW;gBAC9BrD;gBACA2D,KAAK7D,IAAI8D,SAAS;gBAClBrC,KAAKA;gBACL,sEAAsE;gBACtE,oEAAoE;gBACpE,sEAAsE;gBACtE,mEAAmE;gBACnEsC,YAAYnB;YACd;YACA,IAAIe,KAAKK,EAAE,EAAE;oBAe0BL;gBAdrC,MAAMlB,gBAAgB,MAAMlE,gBAAgB6B,WAAWK;gBACvD,mEAAmE;gBACnE,oEAAoE;gBACpE,oEAAoE;gBACpE,sBAAsB;gBACtB,MAAMwD,SAAS,MAAM9D,wBAAwBC,WAAW;oBACtDK;oBACAC,IAAIiD,KAAKjD,EAAE;oBACXR,MAAMyD,KAAKzD,IAAI;oBACfD;gBACF;gBACA,IAAIgE,QAAQ,OAAO9E,iBAAiBwD,OAAO;oBAAEsB;gBAAO;gBACpD,OAAO9E,iBAAiBwD,OAAO;oBAC7BG,SAAS;wBACPX,OAAO;4BAAEzB,IAAIiD,KAAKjD,EAAE;4BAAE8B,SAAS,GAAEmB,kBAAAA,KAAKnB,SAAS,YAAdmB,kBAAkB;4BAAMzD,MAAMyD,KAAKzD,IAAI;4BAAEuC;wBAAc;qBACzF;gBACH;YACF;YACA,IAAIkB,KAAKK,EAAE,KAAK,SAASL,KAAKO,MAAM,KAAK,KAAK;gBAC5C,OAAO/E,iBAAiBwD,OAAO;oBAAEsB,QAAQjF,cAAc2E,KAAKQ,KAAK;gBAAE;YACrE;QACF;QAEA,MAAMC,YAAY,MAAM9F,wBAAwB8B,WAAW;YAAEoB,MAAM;YAAaf;YAAQgB;QAAI;QAC5F,IAAI2C,WAAW,OAAOjF,iBAAiBwD,OAAO;YAAEsB,QAAQjF,cAAcoF;QAAW;QAEjF,IAAIC,QAAiC,EAAE;QACvC,MAAMC,SAAS,MAAM3G,uBAAuB,aAAa;YACvD4G,MAAM;YACN5B;YACA6B,cAAc5E;YACdmD;YACA0B,UAAU;gBAAC;oBAAEC,MAAM;oBAAQC,SAAS5E,qBAAqBC,KAAKC,MAAMC;gBAAM;aAAE;YAC5E0E,MAAM7G;YACNuB,WAAWC,6BAA6BD,SAAS,CAACqD;WAC9ClF,gBAAgB,CAAC,gBAAgB,CAACoH,QAAQ,GAC1C;YAAEA,UAAUpH,gBAAgB,CAAC,gBAAgB,CAACoH,QAAQ;QAAC,IACvD,CAAC,GACDpH,gBAAgB,CAAC,gBAAgB,CAACqH,MAAM,GACxC;YAAEA,QAAQrH,gBAAgB,CAAC,gBAAgB,CAACqH,MAAM;QAAC,IACnD,CAAC;YACLxD,SAAS;gBAAEyD,kBAAkB;YAAK;YAClCC,QAAQ9G,iBAAiB;gBACvB6E;gBACA9C;gBACAgF,SAAS,CAACzE;oBACR6D,QAAQ7D;gBACV;YACF;WACIuB,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAMmD,QAAQpG,kBAAkBwF;QAChC,IAAIA,OAAOJ,MAAM,KAAK,WAAW,OAAO,aAAKgB;YAAOC,SAAS;;QAC7D,IAAIb,OAAOJ,MAAM,KAAK,eAAe,OAAO,aAAKgB;YAAOjB,QAAQpF,iBAAiByF;;QAEjF,MAAMlC,QAAQ,MAAM1D,aAAa0B,WAAW;YAC1CoB,MAAM;YACNf;YACAC,IAAIkC;YACJiB,KAAK7D,IAAI8D,SAAS;YAClBrC;YACAvB;YACAkF,OAAOd,OAAOe,KAAK,CAACD,KAAK;YACzBE,QAAQhB,OAAOe,KAAK,CAACC,MAAM;YAC3BjB;YACAvC;QACF;QACA,IAAIM,MAAM4B,EAAE,KAAK,OAAO;YACtB,IAAI5B,MAAM8B,MAAM,KAAK,KAAK,MAAM,IAAIhC,MAAM,CAAC,KAAK,EAAEzB,OAAO,2CAA2C,CAAC;YACrG,OAAO,aAAKyE;gBAAOjB,QAAQjF,cAAcoD,MAAM+B,KAAK;;QACtD;QACA,yFAAyF;QACzF,MAAM7B,OAAO3D,qBAAqB;YAChC4G,MAAM;gBAAED,QAAQhB,OAAOe,KAAK,CAACC,MAAM;gBAAEF,OAAOd,OAAOe,KAAK,CAACD,KAAK;YAAuC;YACrGrC;YACAY,MAAMU,MAAMmB,GAAG,CAAC,CAACC,OAAU,OAAOA,KAAKC,YAAY,KAAK,WAAWD,KAAKC,YAAY,GAAG;QACzF;QACA,OAAO,aAAKR;YAAOpC,SAAS;gBAACX,OAAOC,OAAOkC,OAAOe,KAAK,CAAChD,IAAI,EAAEC;aAAM;;IACtE;AACF;AAEA,OAAO,MAAMqD,wBAAwBjE,2BAA0B;AAE/D,uGAAuG,GACvG,OAAO,SAASkE;IACdxG,kBAAkB,aAAauG,uBAAuB;QAAEjG,WAAWD;IAAiC;IACpGzB,uBAAuB,aAAaqD;AACtC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { aiPlanEmbedsFor } from '../model/ai-build-plan'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck, aiPlannedComponentProps } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftComponentProps,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a page id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/**\n * The props the confirmed plan gives the component, against the component a\n * COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or\n * when the plan promised no prop to keep.\n *\n * The third kind to need this, after the layout (`aiCopiedLayoutReview`) and\n * the page (`aiCopiedPageReview`), and found the same way: a duplicate branch\n * generates nothing, so it is the one place a plan's promise can go unmet\n * with no model to answer for it.\n *\n * MEASURED 2026-09-21. A plan read \"the existing card has no link target; the\n * brief needs one\", and `practice-area-link-card` came out BYTE-IDENTICAL to\n * `practice-area-card` — the same seven nodes under the same ids, the same\n * three props, no link anywhere. Its job reported the copy as built. So did\n * `practice-area-link-card-v2` the following pass. A component job can\n * therefore charge a member for a duplicate of something they already had.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedComponentReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = aiPlannedComponentProps(input.plan)\n if (!promised.length) return null\n const declared = await readAiDraftComponentProps(firestore, { hostId: input.hostId, id: input.id })\n if (declared === null) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,\n findings: [],\n }\n }\n const missing = promised.filter((name) => !declared.includes(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a property' : 'properties'\n } the copy does not have.`,\n violations: [\n {\n rule: null,\n code: 'plan-props-missing',\n message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${\n declared.length ? declared.join(', ') : 'none of them'\n }. Add ${one ? 'it' : 'each of them'}, and use ${\n one ? 'it' : 'them'\n } in the component, or the copy is the component it was copied from.`,\n },\n ],\n })\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so without this a plan\n // reading \"duplicate it and add a link\" reports Done on a duplicate\n // that added nothing.\n const review = await aiCopiedComponentReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n // A third-party player the confirmed plan lists for this component is the only one it may embed (AGL-3433).\n context: {\n definesComponent: true,\n ...(creation ? { plannedEmbeds: aiPlanEmbedsFor(plan, { create: creation.name }) } : {}),\n },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","aiPlanEmbedsFor","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiPlannedComponentProps","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftComponentProps","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiCopiedComponentReview","firestore","input","promised","length","declared","hostId","id","reason","message","findings","missing","filter","includes","one","violations","rule","code","aiComponentJobAdmission","context","orgId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","plannedEmbeds","create","extend","onProps","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
@@ -172,8 +172,16 @@ export declare function aiDraftAllowanceRefusal(firestore: Firestore, input: {
172
172
  * or the routing map already serves — so the member's publish at it later
173
173
  * takes nothing from a live page. The root is asked for only while no screen
174
174
  * holds it.
175
+ *
176
+ * Except the placeholder home page a site is created with (AGL-3408), which
177
+ * holds `/` only until a real home page is published there: the guided start
178
+ * is offered to exactly the sites that still have it, and the home page it
179
+ * drafts has to be able to ask for the root. Publishing it replaces the
180
+ * placeholder in the same write.
175
181
  */
176
- export declare function aiDraftScreenSlug(requested: string | null | undefined, name: string, rows: ReadonlyArray<Pick<SiblingRow, 'slug' | 'deleted'>>, routingMap: RoutingMap): string;
182
+ export declare function aiDraftScreenSlug(requested: string | null | undefined, name: string, rows: ReadonlyArray<Pick<SiblingRow, 'slug' | 'deleted'> & {
183
+ id?: string;
184
+ }>, routingMap: RoutingMap, defaultHomeScreenId?: string | null): string;
177
185
  export interface AiDraftRecord {
178
186
  id: string;
179
187
  /** A layout's or a screen's first version; `null` for every other kind, which the writer gives none. */
@@ -117,7 +117,7 @@ import { aiUnrestrictedPlanCapabilities } from "../model/ai-plan-capabilities.js
117
117
  screen: {
118
118
  collection: 'screens',
119
119
  quotaKey: 'screensPerHost',
120
- label: 'screens'
120
+ label: 'pages'
121
121
  }
122
122
  };
123
123
  function draftCollection(firestore, hostId, kind) {
@@ -274,11 +274,18 @@ function subdomainOf(host) {
274
274
  * or the routing map already serves — so the member's publish at it later
275
275
  * takes nothing from a live page. The root is asked for only while no screen
276
276
  * holds it.
277
- */ export function aiDraftScreenSlug(requested, name, rows, routingMap) {
277
+ *
278
+ * Except the placeholder home page a site is created with (AGL-3408), which
279
+ * holds `/` only until a real home page is published there: the guided start
280
+ * is offered to exactly the sites that still have it, and the home page it
281
+ * drafts has to be able to ask for the root. Publishing it replaces the
282
+ * placeholder in the same write.
283
+ */ export function aiDraftScreenSlug(requested, name, rows, routingMap, defaultHomeScreenId) {
278
284
  var _ref, _usable;
285
+ const placeholder = (id, path)=>Boolean(defaultHomeScreenId) && id === defaultHomeScreenId && path === SCREEN_ROOT_PATH;
279
286
  const taken = new Set([
280
- ...rows.filter((row)=>!row.deleted && row.slug).map((row)=>row.slug),
281
- ...Object.values(routingMap != null ? routingMap : {}).filter((path)=>typeof path === 'string')
287
+ ...rows.filter((row)=>!row.deleted && row.slug && !placeholder(row.id, row.slug)).map((row)=>row.slug),
288
+ ...Object.entries(routingMap != null ? routingMap : {}).filter(([id, path])=>typeof path === 'string' && !placeholder(id, path)).map(([, path])=>path)
282
289
  ]);
283
290
  const usable = (value)=>value && !reservedScreenRouteSegment(value) && !(value === SCREEN_ROOT_PATH && taken.has(value)) ? value : undefined;
284
291
  const base = (_ref = (_usable = usable(normalizeScreenSlug(requested != null ? requested : undefined))) != null ? _usable : usable(normalizeScreenSlug(name))) != null ? _ref : 'page';
@@ -424,7 +431,7 @@ function recordOf(id, draft, host) {
424
431
  }, stamps));
425
432
  } else if (input.kind === 'screen') {
426
433
  versionId = createResourceUid();
427
- const slug = aiDraftScreenSlug(input.slug, name, rows, routingMap);
434
+ const slug = aiDraftScreenSlug(input.slug, name, rows, routingMap, host.get('defaultHomeScreenId'));
428
435
  tx.create(draftRef, _extends({}, allowListed('screen', {
429
436
  displayName: name,
430
437
  slug,