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

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 (181) hide show
  1. package/package.json +18 -18
  2. package/src/lib/billing/ai-month-meter.d.ts +51 -0
  3. package/src/lib/billing/ai-month-meter.js +89 -0
  4. package/src/lib/billing/ai-month-meter.js.map +1 -0
  5. package/src/lib/billing/ai-overage-close.d.ts +11 -33
  6. package/src/lib/billing/ai-overage-close.js +4 -3
  7. package/src/lib/billing/ai-overage-close.js.map +1 -1
  8. package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
  9. package/src/lib/billing/ai-overage-cutover.js +1 -28
  10. package/src/lib/billing/ai-overage-cutover.js.map +1 -1
  11. package/src/lib/billing/ai-overage-ledger.js +1 -1
  12. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  13. package/src/lib/billing/ai-overage-standing.js +1 -1
  14. package/src/lib/billing/ai-overage-standing.js.map +1 -1
  15. package/src/lib/components/ai-allotment-editor.component.js +1 -1
  16. package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
  17. package/src/lib/components/ai-assist-provider.component.js +3 -2
  18. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  19. package/src/lib/components/ai-automation-zones.d.ts +63 -0
  20. package/src/lib/components/ai-automation-zones.js +31 -0
  21. package/src/lib/components/ai-automation-zones.js.map +1 -0
  22. package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
  23. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  24. package/src/lib/components/ai-describe-button.component.d.ts +6 -3
  25. package/src/lib/components/ai-describe-button.component.js +4 -1
  26. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  27. package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
  28. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  29. package/src/lib/components/billing-ai-allotments.component.js +1 -1
  30. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  31. package/src/lib/components/billing-assist-overage-card.component.js +1 -1
  32. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  33. package/src/lib/constants.d.ts +5 -0
  34. package/src/lib/constants.js +4 -0
  35. package/src/lib/constants.js.map +1 -1
  36. package/src/lib/declarations.js +2 -0
  37. package/src/lib/declarations.js.map +1 -1
  38. package/src/lib/declarations.server.js +24 -10
  39. package/src/lib/declarations.server.js.map +1 -1
  40. package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
  41. package/src/lib/insights/ai-dataset-catalog.js +57 -0
  42. package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
  43. package/src/lib/insights/ai-figure-readers.d.ts +6 -39
  44. package/src/lib/insights/ai-figure-readers.js +2 -391
  45. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  46. package/src/lib/insights/ai-insight-digest.d.ts +10 -1
  47. package/src/lib/insights/ai-insight-digest.js +16 -4
  48. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  49. package/src/lib/insights/ai-insight-readers.d.ts +1 -1
  50. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  51. package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
  52. package/src/lib/jobs/ai-job-component-step.js +23 -6
  53. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-draft-ids.js +11 -0
  55. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  56. package/src/lib/jobs/ai-job-email-step.d.ts +5 -1
  57. package/src/lib/jobs/ai-job-email-step.js +10 -1
  58. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  59. package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
  60. package/src/lib/jobs/ai-job-form-step.js +60 -8
  61. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-generation.d.ts +21 -0
  63. package/src/lib/jobs/ai-job-generation.js +53 -1
  64. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  65. package/src/lib/jobs/ai-job-insight-step.js +1 -1
  66. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
  68. package/src/lib/jobs/ai-job-layout-step.js +45 -12
  69. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
  71. package/src/lib/jobs/ai-job-plan-conformance.js +7 -1
  72. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  73. package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
  74. package/src/lib/jobs/ai-job-plan-creations.js +199 -0
  75. package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
  76. package/src/lib/jobs/ai-job-plan-step.d.ts +8 -2
  77. package/src/lib/jobs/ai-job-plan-step.js +20 -6
  78. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  79. package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
  80. package/src/lib/jobs/ai-job-site-step.js +6 -6
  81. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  82. package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
  83. package/src/lib/jobs/ai-job-template-step.js +72 -15
  84. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  85. package/src/lib/jobs/ai-jobs.js +1 -1
  86. package/src/lib/jobs/ai-jobs.js.map +1 -1
  87. package/src/lib/jobs/ai-workflow-records.d.ts +5 -4
  88. package/src/lib/jobs/ai-workflow-records.js +26 -22
  89. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  90. package/src/lib/model/ai-allotments.d.ts +16 -3
  91. package/src/lib/model/ai-allotments.js +34 -11
  92. package/src/lib/model/ai-allotments.js.map +1 -1
  93. package/src/lib/model/ai-job-creations.d.ts +55 -0
  94. package/src/lib/model/ai-job-creations.js +85 -0
  95. package/src/lib/model/ai-job-creations.js.map +1 -0
  96. package/src/lib/model/ai-page-job.d.ts +3 -2
  97. package/src/lib/model/ai-page-job.js +4 -3
  98. package/src/lib/model/ai-page-job.js.map +1 -1
  99. package/src/lib/model/ai-site-job.d.ts +4 -2
  100. package/src/lib/model/ai-site-job.js +8 -4
  101. package/src/lib/model/ai-site-job.js.map +1 -1
  102. package/src/lib/model/ai-template-subjects.d.ts +10 -0
  103. package/src/lib/model/ai-template-subjects.js +14 -0
  104. package/src/lib/model/ai-template-subjects.js.map +1 -1
  105. package/src/lib/model/ai-tokens.js +1 -1
  106. package/src/lib/model/ai-tokens.js.map +1 -1
  107. package/src/lib/model/ai-usage-by-user.js +1 -1
  108. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  109. package/src/lib/providers/catalog.js +1 -1
  110. package/src/lib/providers/catalog.js.map +1 -1
  111. package/src/lib/providers/model-choice.js +1 -1
  112. package/src/lib/providers/model-choice.js.map +1 -1
  113. package/src/lib/runtime/ai-device-audit.d.ts +1 -1
  114. package/src/lib/runtime/ai-device-audit.js +1 -1
  115. package/src/lib/runtime/ai-device-audit.js.map +1 -1
  116. package/src/lib/runtime/ai-eval-live.js +1 -1
  117. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  118. package/src/lib/runtime/ai-eval.d.ts +1 -1
  119. package/src/lib/runtime/ai-eval.js.map +1 -1
  120. package/src/lib/runtime/ai-gate.js +1 -1
  121. package/src/lib/runtime/ai-gate.js.map +1 -1
  122. package/src/lib/runtime/ai-palette.d.ts +1 -1
  123. package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
  124. package/src/lib/runtime/ai-palette.generated.js +1 -1
  125. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  126. package/src/lib/runtime/ai-palette.js.map +1 -1
  127. package/src/lib/runtime/site-inventory.js +28 -22
  128. package/src/lib/runtime/site-inventory.js.map +1 -1
  129. package/src/lib/server/ai-admin-org.js +1 -1
  130. package/src/lib/server/ai-admin-org.js.map +1 -1
  131. package/src/lib/server/ai-admin-orgs-spend.js +1 -1
  132. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  133. package/src/lib/server/ai-admin-signals.js +1 -1
  134. package/src/lib/server/ai-admin-signals.js.map +1 -1
  135. package/src/lib/server/ai-allotments.js +1 -1
  136. package/src/lib/server/ai-allotments.js.map +1 -1
  137. package/src/lib/server/ai-assist.js +2 -2
  138. package/src/lib/server/ai-assist.js.map +1 -1
  139. package/src/lib/server/assist-chat.d.ts +1 -1
  140. package/src/lib/server/assist-chat.js +2 -2
  141. package/src/lib/server/assist-chat.js.map +1 -1
  142. package/src/lib/server/billing-credits.d.ts +5 -0
  143. package/src/lib/server/billing-credits.js +8 -2
  144. package/src/lib/server/billing-credits.js.map +1 -1
  145. package/src/lib/server/billing-overage.js +2 -2
  146. package/src/lib/server/billing-overage.js.map +1 -1
  147. package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
  148. package/src/lib/testing/stand-in-dataset-index.js +80 -0
  149. package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
  150. package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
  151. package/src/lib/usage/ai-allotment-alerts.js +31 -14
  152. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  153. package/src/lib/usage/ai-quota-checks.d.ts +37 -0
  154. package/src/lib/usage/ai-quota-checks.js +69 -0
  155. package/src/lib/usage/ai-quota-checks.js.map +1 -0
  156. package/src/lib/usage/ai-usage-alerts.js +22 -10
  157. package/src/lib/usage/ai-usage-alerts.js.map +1 -1
  158. package/src/lib/usage/ai-usage-by-user.js +1 -1
  159. package/src/lib/usage/ai-usage-by-user.js.map +1 -1
  160. package/src/lib/usage/assist-ceiling.d.ts +2 -2
  161. package/src/lib/usage/assist-ceiling.js +4 -4
  162. package/src/lib/usage/assist-ceiling.js.map +1 -1
  163. package/src/lib/usage/assist-credits.d.ts +494 -0
  164. package/src/lib/usage/assist-credits.js +524 -0
  165. package/src/lib/usage/assist-credits.js.map +1 -0
  166. package/src/lib/usage/assist-free-taste.d.ts +1 -1
  167. package/src/lib/usage/assist-free-taste.js +1 -1
  168. package/src/lib/usage/assist-free-taste.js.map +1 -1
  169. package/src/lib/usage/assist-signal-mining.js +1 -1
  170. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  171. package/src/lib/usage/assist-spend-guards.d.ts +153 -0
  172. package/src/lib/usage/assist-spend-guards.js +182 -0
  173. package/src/lib/usage/assist-spend-guards.js.map +1 -0
  174. package/src/lib/usage/assist-usage.d.ts +1 -1
  175. package/src/lib/usage/assist-usage.js +1 -1
  176. package/src/lib/usage/assist-usage.js.map +1 -1
  177. package/src/lib/usage/staff-org-ai.js +1 -1
  178. package/src/lib/usage/staff-org-ai.js.map +1 -1
  179. package/src/lib/usage-axes.d.ts +10 -0
  180. package/src/lib/usage-axes.js +40 -2
  181. package/src/lib/usage-axes.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-draft-ids.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { AiBuildPlan, AiBuildPlanCreateKind } from '../model/ai-build-plan'\nimport type { AiJob, AiJobDraftSlot, AiJobKind } from '../model/ai-jobs.types'\n\n/**\n * The ids a job's drafts are written under (AGL-3079).\n *\n * A draft a job writes is the resource a member would otherwise have made in\n * the console, so it is named the way the console names one: by\n * `createResourceUid()`. Each id is minted ONCE and recorded on the job before\n * the draft is written. That record is what lets a step run again safely: a\n * run cut off after its write reads the same id, finds the draft, and reports\n * it rather than writing a second.\n *\n * Where an id is recorded follows what decides the draft exists:\n *\n * - A draft the job's KIND always writes — a layout job's layout, a page\n * job's screen, a campaign's design and campaign — is minted when the job\n * is created, on the step that writes it (`AiJobStep.draftIds`).\n * - A draft the PLAN decides — what a page job's plan creates, a scaffold's\n * layout, form and pages — is minted when the plan is kept, on the plan's\n * own entry (`AiBuildPlanCreate.id`, `AiBuildPlanScreen.id`). Those are\n * built one unit a pass by the step that owns their kind, under a job named\n * by the entry's id (`aiSiteUnitJob`).\n *\n * A job whose step records no id names its draft by the job's own id, which\n * is the id every draft of such a job was written under.\n */\n\n/** The drafts each kind's step writes of its own, whatever its plan says. */\nexport const AI_JOB_DRAFT_SLOTS: Readonly<Partial<Record<AiJobKind, readonly AiJobDraftSlot[]>>> = {\n page: ['screen'],\n layout: ['layout'],\n template: ['template'],\n form: ['form'],\n component: ['component'],\n email: ['email'],\n campaign: ['email', 'campaign'],\n workflow: ['workflow'],\n // The welcome email a scaffold builds; its pages and creations are its plan's.\n site: ['email'],\n}\n\n/** Fresh ids for the drafts a job of this kind writes of its own; `null` for a kind that writes none. */\nexport function aiMintJobDraftIds(kind: AiJobKind): Partial<Record<AiJobDraftSlot, string>> | null {\n const slots = AI_JOB_DRAFT_SLOTS[kind]\n if (!slots?.length) return null\n return Object.fromEntries(slots.map((slot) => [slot, createResourceUid()]))\n}\n\n/** The id recorded on the job's steps for its draft of `slot`; `null` where none is. */\nexport function aiRecordedJobDraftId(job: Pick<AiJob, 'steps'>, slot: AiJobDraftSlot): string | null {\n for (const step of job.steps ?? []) {\n const id = step.draftIds?.[slot]\n if (typeof id === 'string' && id) return id\n }\n return null\n}\n\n/**\n * The id the job writes its draft of `slot` under, and finds it by when the\n * step runs again: the id recorded on its step, else the job's own id — which\n * names a unit's job by its plan entry's id, and a job whose step records none.\n */\nexport function aiJobDraftId(job: Pick<AiJob, '$id' | 'steps'>, slot: AiJobDraftSlot): string {\n return aiRecordedJobDraftId(job, slot) ?? job.$id\n}\n\n/** The creations a job writes as documents: a theme change is a proposal, and no job writes a dataset. */\nconst DRAFT_CREATION_KINDS: ReadonlySet<AiBuildPlanCreateKind> = new Set<AiBuildPlanCreateKind>([\n 'component',\n 'form',\n 'layout',\n 'template',\n 'email',\n])\n\n/** The kinds whose step builds what their plan names, one unit a draft, and whether that includes its screens. */\nconst UNIT_PLAN_KINDS: Readonly<Partial<Record<AiJobKind, { screens: boolean }>>> = {\n // A page job's own screen is its kind's draft; its plan's creations are units.\n page: { screens: false },\n site: { screens: true },\n}\n\n/**\n * The plan as a job of `kind` keeps it: for a kind that builds what its plan\n * names, a fresh id on every creation it writes as a document and, for a\n * scaffold, on every screen. An id the plan already carried is replaced —\n * a plan reused from another job names that job's drafts — so the drafts\n * this job builds are its own. Any other kind's plan is kept as it is.\n */\nexport function aiPlanWithDraftIds<T extends AiBuildPlan>(kind: AiJobKind, plan: T): T {\n const units = UNIT_PLAN_KINDS[kind]\n if (!units) return plan\n return {\n ...plan,\n create: plan.create.map((entry) =>\n DRAFT_CREATION_KINDS.has(entry.kind) ? { ...entry, id: createResourceUid() } : entry,\n ),\n screens: units.screens ? plan.screens.map((screen) => ({ ...screen, id: createResourceUid() })) : plan.screens,\n }\n}\n"],"names":["createResourceUid","AI_JOB_DRAFT_SLOTS","page","layout","template","form","component","email","campaign","workflow","site","aiMintJobDraftIds","kind","slots","length","Object","fromEntries","map","slot","aiRecordedJobDraftId","job","step","steps","id","draftIds","aiJobDraftId","$id","DRAFT_CREATION_KINDS","Set","UNIT_PLAN_KINDS","screens","aiPlanWithDraftIds","plan","units","create","entry","has","screen"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAI9E;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,2EAA2E,GAC3E,OAAO,MAAMC,qBAAsF;IACjGC,MAAM;QAAC;KAAS;IAChBC,QAAQ;QAAC;KAAS;IAClBC,UAAU;QAAC;KAAW;IACtBC,MAAM;QAAC;KAAO;IACdC,WAAW;QAAC;KAAY;IACxBC,OAAO;QAAC;KAAQ;IAChBC,UAAU;QAAC;QAAS;KAAW;IAC/BC,UAAU;QAAC;KAAW;IACtB,+EAA+E;IAC/EC,MAAM;QAAC;KAAQ;AACjB,EAAC;AAED,uGAAuG,GACvG,OAAO,SAASC,kBAAkBC,IAAe;IAC/C,MAAMC,QAAQZ,kBAAkB,CAACW,KAAK;IACtC,IAAI,EAACC,yBAAAA,MAAOC,MAAM,GAAE,OAAO;IAC3B,OAAOC,OAAOC,WAAW,CAACH,MAAMI,GAAG,CAAC,CAACC,OAAS;YAACA;YAAMlB;SAAoB;AAC3E;AAEA,sFAAsF,GACtF,OAAO,SAASmB,qBAAqBC,GAAyB,EAAEF,IAAoB;QAC/DE;IAAnB,KAAK,MAAMC,SAAQD,aAAAA,IAAIE,KAAK,YAATF,aAAa,EAAE,CAAE;YACvBC;QAAX,MAAME,MAAKF,iBAAAA,KAAKG,QAAQ,qBAAbH,cAAe,CAACH,KAAK;QAChC,IAAI,OAAOK,OAAO,YAAYA,IAAI,OAAOA;IAC3C;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,aAAaL,GAAiC,EAAEF,IAAoB;QAC3EC;IAAP,QAAOA,wBAAAA,qBAAqBC,KAAKF,iBAA1BC,wBAAmCC,IAAIM,GAAG;AACnD;AAEA,wGAAwG,GACxG,MAAMC,uBAA2D,IAAIC,IAA2B;IAC9F;IACA;IACA;IACA;IACA;CACD;AAED,gHAAgH,GAChH,MAAMC,kBAA8E;IAClF,+EAA+E;IAC/E3B,MAAM;QAAE4B,SAAS;IAAM;IACvBpB,MAAM;QAAEoB,SAAS;IAAK;AACxB;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,mBAA0CnB,IAAe,EAAEoB,IAAO;IAChF,MAAMC,QAAQJ,eAAe,CAACjB,KAAK;IACnC,IAAI,CAACqB,OAAO,OAAOD;IACnB,OAAO,aACFA;QACHE,QAAQF,KAAKE,MAAM,CAACjB,GAAG,CAAC,CAACkB,QACvBR,qBAAqBS,GAAG,CAACD,MAAMvB,IAAI,IAAI,aAAKuB;gBAAOZ,IAAIvB;iBAAwBmC;QAEjFL,SAASG,MAAMH,OAAO,GAAGE,KAAKF,OAAO,CAACb,GAAG,CAAC,CAACoB,SAAY,aAAKA;gBAAQd,IAAIvB;kBAA0BgC,KAAKF,OAAO;;AAElH"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-draft-ids.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { AiBuildPlan, AiBuildPlanCreateKind } from '../model/ai-build-plan'\nimport type { AiJob, AiJobDraftSlot, AiJobKind } from '../model/ai-jobs.types'\n\n/**\n * The ids a job's drafts are written under (AGL-3079).\n *\n * A draft a job writes is the resource a member would otherwise have made in\n * the console, so it is named the way the console names one: by\n * `createResourceUid()`. Each id is minted ONCE and recorded on the job before\n * the draft is written. That record is what lets a step run again safely: a\n * run cut off after its write reads the same id, finds the draft, and reports\n * it rather than writing a second.\n *\n * Where an id is recorded follows what decides the draft exists:\n *\n * - A draft the job's KIND always writes — a layout job's layout, a page\n * job's screen, a campaign's design and campaign — is minted when the job\n * is created, on the step that writes it (`AiJobStep.draftIds`).\n * - A draft the PLAN decides — what a page job's plan creates, a scaffold's\n * layout, form and pages — is minted when the plan is kept, on the plan's\n * own entry (`AiBuildPlanCreate.id`, `AiBuildPlanScreen.id`). Those are\n * built one unit a pass by the step that owns their kind, under a job named\n * by the entry's id (`aiSiteUnitJob`).\n *\n * A job whose step records no id names its draft by the job's own id, which\n * is the id every draft of such a job was written under.\n */\n\n/** The drafts each kind's step writes of its own, whatever its plan says. */\nexport const AI_JOB_DRAFT_SLOTS: Readonly<Partial<Record<AiJobKind, readonly AiJobDraftSlot[]>>> = {\n page: ['screen'],\n layout: ['layout'],\n template: ['template'],\n form: ['form'],\n component: ['component'],\n email: ['email'],\n campaign: ['email', 'campaign'],\n workflow: ['workflow'],\n // The welcome email a scaffold builds; its pages and creations are its plan's.\n site: ['email'],\n}\n\n/** Fresh ids for the drafts a job of this kind writes of its own; `null` for a kind that writes none. */\nexport function aiMintJobDraftIds(kind: AiJobKind): Partial<Record<AiJobDraftSlot, string>> | null {\n const slots = AI_JOB_DRAFT_SLOTS[kind]\n if (!slots?.length) return null\n return Object.fromEntries(slots.map((slot) => [slot, createResourceUid()]))\n}\n\n/** The id recorded on the job's steps for its draft of `slot`; `null` where none is. */\nexport function aiRecordedJobDraftId(job: Pick<AiJob, 'steps'>, slot: AiJobDraftSlot): string | null {\n for (const step of job.steps ?? []) {\n const id = step.draftIds?.[slot]\n if (typeof id === 'string' && id) return id\n }\n return null\n}\n\n/**\n * The id the job writes its draft of `slot` under, and finds it by when the\n * step runs again: the id recorded on its step, else the job's own id — which\n * names a unit's job by its plan entry's id, and a job whose step records none.\n */\nexport function aiJobDraftId(job: Pick<AiJob, '$id' | 'steps'>, slot: AiJobDraftSlot): string {\n return aiRecordedJobDraftId(job, slot) ?? job.$id\n}\n\n/** The creations a job writes as documents: a theme change is a proposal, and no job writes a dataset. */\nconst DRAFT_CREATION_KINDS: ReadonlySet<AiBuildPlanCreateKind> = new Set<AiBuildPlanCreateKind>([\n 'component',\n 'form',\n 'layout',\n 'template',\n 'email',\n])\n\n/** The kinds whose step builds what their plan names, one unit a draft, and whether that includes its screens. */\nconst UNIT_PLAN_KINDS: Readonly<Partial<Record<AiJobKind, { screens: boolean }>>> = {\n // A page job's own screen is its kind's draft; its plan's creations are units.\n page: { screens: false },\n site: { screens: true },\n // So are the components and forms a template, layout or component job\n // builds before its own record (AGL-3143 §15).\n template: { screens: false },\n layout: { screens: false },\n component: { screens: false },\n}\n\n/**\n * The plan as a job of `kind` keeps it: for a kind that builds what its plan\n * names, a fresh id on every creation it writes as a document and, for a\n * scaffold, on every screen. An id the plan already carried is replaced —\n * a plan reused from another job names that job's drafts — so the drafts\n * this job builds are its own. Any other kind's plan is kept as it is.\n */\nexport function aiPlanWithDraftIds<T extends AiBuildPlan>(kind: AiJobKind, plan: T): T {\n const units = UNIT_PLAN_KINDS[kind]\n if (!units) return plan\n return {\n ...plan,\n create: plan.create.map((entry) =>\n DRAFT_CREATION_KINDS.has(entry.kind) ? { ...entry, id: createResourceUid() } : entry,\n ),\n screens: units.screens ? plan.screens.map((screen) => ({ ...screen, id: createResourceUid() })) : plan.screens,\n }\n}\n"],"names":["createResourceUid","AI_JOB_DRAFT_SLOTS","page","layout","template","form","component","email","campaign","workflow","site","aiMintJobDraftIds","kind","slots","length","Object","fromEntries","map","slot","aiRecordedJobDraftId","job","step","steps","id","draftIds","aiJobDraftId","$id","DRAFT_CREATION_KINDS","Set","UNIT_PLAN_KINDS","screens","aiPlanWithDraftIds","plan","units","create","entry","has","screen"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,6CAA4C;AAI9E;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,2EAA2E,GAC3E,OAAO,MAAMC,qBAAsF;IACjGC,MAAM;QAAC;KAAS;IAChBC,QAAQ;QAAC;KAAS;IAClBC,UAAU;QAAC;KAAW;IACtBC,MAAM;QAAC;KAAO;IACdC,WAAW;QAAC;KAAY;IACxBC,OAAO;QAAC;KAAQ;IAChBC,UAAU;QAAC;QAAS;KAAW;IAC/BC,UAAU;QAAC;KAAW;IACtB,+EAA+E;IAC/EC,MAAM;QAAC;KAAQ;AACjB,EAAC;AAED,uGAAuG,GACvG,OAAO,SAASC,kBAAkBC,IAAe;IAC/C,MAAMC,QAAQZ,kBAAkB,CAACW,KAAK;IACtC,IAAI,EAACC,yBAAAA,MAAOC,MAAM,GAAE,OAAO;IAC3B,OAAOC,OAAOC,WAAW,CAACH,MAAMI,GAAG,CAAC,CAACC,OAAS;YAACA;YAAMlB;SAAoB;AAC3E;AAEA,sFAAsF,GACtF,OAAO,SAASmB,qBAAqBC,GAAyB,EAAEF,IAAoB;QAC/DE;IAAnB,KAAK,MAAMC,SAAQD,aAAAA,IAAIE,KAAK,YAATF,aAAa,EAAE,CAAE;YACvBC;QAAX,MAAME,MAAKF,iBAAAA,KAAKG,QAAQ,qBAAbH,cAAe,CAACH,KAAK;QAChC,IAAI,OAAOK,OAAO,YAAYA,IAAI,OAAOA;IAC3C;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,aAAaL,GAAiC,EAAEF,IAAoB;QAC3EC;IAAP,QAAOA,wBAAAA,qBAAqBC,KAAKF,iBAA1BC,wBAAmCC,IAAIM,GAAG;AACnD;AAEA,wGAAwG,GACxG,MAAMC,uBAA2D,IAAIC,IAA2B;IAC9F;IACA;IACA;IACA;IACA;CACD;AAED,gHAAgH,GAChH,MAAMC,kBAA8E;IAClF,+EAA+E;IAC/E3B,MAAM;QAAE4B,SAAS;IAAM;IACvBpB,MAAM;QAAEoB,SAAS;IAAK;IACtB,sEAAsE;IACtE,+CAA+C;IAC/C1B,UAAU;QAAE0B,SAAS;IAAM;IAC3B3B,QAAQ;QAAE2B,SAAS;IAAM;IACzBxB,WAAW;QAAEwB,SAAS;IAAM;AAC9B;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,mBAA0CnB,IAAe,EAAEoB,IAAO;IAChF,MAAMC,QAAQJ,eAAe,CAACjB,KAAK;IACnC,IAAI,CAACqB,OAAO,OAAOD;IACnB,OAAO,aACFA;QACHE,QAAQF,KAAKE,MAAM,CAACjB,GAAG,CAAC,CAACkB,QACvBR,qBAAqBS,GAAG,CAACD,MAAMvB,IAAI,IAAI,aAAKuB;gBAAOZ,IAAIvB;iBAAwBmC;QAEjFL,SAASG,MAAMH,OAAO,GAAGE,KAAKF,OAAO,CAACb,GAAG,CAAC,CAACoB,SAAY,aAAKA;gBAAQd,IAAIvB;kBAA0BgC,KAAKF,OAAO;;AAElH"}
@@ -179,7 +179,11 @@ export declare function aiEmailDesignOutput(record: PluginDraftRecord, place: {
179
179
  hostId: string;
180
180
  hostSubdomain: string | null;
181
181
  }, note: string | null): AiJobOutput;
182
- /** An email job is admitted for a site of its org with email on and room for a design. */
182
+ /**
183
+ * An email job is admitted for a site of its org with email on and room for a
184
+ * design, and confirmed only for a plan that creates nothing but the design:
185
+ * an email places nothing another job builds (AGL-3143 §15).
186
+ */
183
187
  export declare const aiEmailJobAdmission: AiJobAdmission;
184
188
  export interface AiJobEmailStepDeps {
185
189
  /** The inventory reader; specs hand in a fake. */
@@ -22,6 +22,7 @@ import { AI_TEXT_LIMITS } from "../runtime/ai-palette.js";
22
22
  import { readSiteInventory } from "../runtime/site-inventory.js";
23
23
  import { aiEmailProductNote, resolveAiEmailProducts } from "./ai-email-bindings.js";
24
24
  import { registerAiJobAdmission } from "./ai-job-admission.js";
25
+ import { aiJobPlanCreationsRefusal } from "./ai-job-plan-creations.js";
25
26
  import { aiJobStepBudget } from "./ai-job-budget.js";
26
27
  import { aiJobDraftId } from "./ai-job-draft-ids.js";
27
28
  import { aiSiteSubdomain } from "./ai-job-drafts.js";
@@ -417,7 +418,12 @@ function visitsOf(tree) {
417
418
  note
418
419
  } : {});
419
420
  }
420
- /** An email job is admitted for a site of its org with email on and room for a design. */ export const aiEmailJobAdmission = (context)=>aiPluginDraftAdmissionRefusal(context, {
421
+ /**
422
+ * An email job is admitted for a site of its org with email on and room for a
423
+ * design, and confirmed only for a plan that creates nothing but the design:
424
+ * an email places nothing another job builds (AGL-3143 §15).
425
+ */ export const aiEmailJobAdmission = async (context)=>{
426
+ const refusal = await aiPluginDraftAdmissionRefusal(context, {
421
427
  kind: 'email',
422
428
  drafts: [
423
429
  {
@@ -426,6 +432,9 @@ function visitsOf(tree) {
426
432
  }
427
433
  ]
428
434
  });
435
+ if (refusal || !context.hostId) return refusal;
436
+ return aiJobPlanCreationsRefusal('email', context, context.hostId);
437
+ };
429
438
  export function createAiJobEmailStep(deps = {}) {
430
439
  var _deps_readInventory, _deps_writerFor;
431
440
  const readInventory = (_deps_readInventory = deps.readInventory) != null ? _deps_readInventory : readSiteInventory;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-email-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 {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiEmailProductNote,\n resolveAiEmailProducts,\n type AiEmailProductBinding,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget, type AiJobStepBudget } from './ai-job-budget'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\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 email step (AGL-2912): the generation step of an `email` job, run once a\n * member confirmed the job's plan, and the generation the campaign step\n * drafts its email with.\n *\n * It builds ONE email design from the email palette — sections holding text,\n * buttons, images, dividers, spacers and product blocks, never hand-written\n * HTML, which is not on the email surface — through\n * `runValidatedGeneration('email', …)`, so every building rule holds on the\n * tree, rule 6's brand and rule 17's clip size included. Beside the design the\n * answer carries three subject lines and three preheaders. The door's checks\n * ride `extend`:\n *\n * - every block sits in a section, every button links somewhere, and the\n * copy's only merge tokens are the recipient's own, in text;\n * - exactly as many product blocks as the job binds products;\n * - three different subject lines and preheaders, within their lengths and\n * free of merge tokens, which a subject line does not fill;\n * - and the design RENDERS: the email plugin's writer checks it through the\n * send path's own renderer, reached through the core's resource-drafts\n * seam, and a problem it names is a violation the model is re-asked with.\n *\n * What the model is sent is the brief, the email's name and kind, the\n * confirmed plan and the site inventory, and how many product blocks to place.\n * The products themselves are bound by id after it answers\n * (`ai-email-bindings.ts`); a link to one of the site's pages is completed\n * with the site's address token, which the send fills.\n *\n * The design lands through that same writer: a draft email screen, listed on\n * the Emails page's templates, that no campaign sends until a member picks it.\n * The job never sends, schedules or publishes anything.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under a tighter\n * answer ceiling than the doctrine's default for an email, lowered on a tier\n * too slow to answer it inside that time.\n */\n\n/** The resource the email plugin writes an email design under. */\nexport const AI_EMAIL_DESIGN_RESOURCE = 'emailDesign'\n\n/** The longest answer an email may run to; the tree is held to the email budget either way. */\nexport const AI_JOB_EMAIL_MAX_TOKENS = AI_ROUTING_TABLE['job.email'].maxTokens\n\n/**\n * The time an email generation takes (AGL-3035, AGL-3036), for each step kind\n * that runs one: its two inventory-lookup rounds, its answer and its re-ask at\n * the kind's routing ceiling on the tier the kind is served from, fitted to\n * what a beat can give a step. The email step registers the first, and the\n * campaign step — whose email is this same generation — the second.\n */\nexport const AI_JOB_EMAIL_STEP_BUDGETS: Readonly<\n Record<AiEmailGenerationInput['step'], AiJobStepBudget>\n> = {\n 'job.email': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.email'],\n maxTokens: AI_ROUTING_TABLE['job.email'].maxTokens,\n }),\n 'job.campaign': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.campaign'],\n maxTokens: AI_ROUTING_TABLE['job.campaign'].maxTokens,\n }),\n}\n\n/** The least time one email step needs before it starts. */\nexport const AI_JOB_EMAIL_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.email'].minimumMs\n\n/** How many subject lines, and how many preheaders, an email is written with. */\nexport const AI_EMAIL_VARIANTS = 3\n\n/** A subject line is a headline. */\nexport const AI_EMAIL_SUBJECT_MAX_CHARS = AI_TEXT_LIMITS.headline\n\n/** The longest preheader the campaign send route stores. */\nexport const AI_EMAIL_PREHEADER_MAX_CHARS = 200\n\n/** An email's name when neither the plan nor the job names one. */\nexport const AI_JOB_EMAIL_DEFAULT_NAME = 'New email'\n\n/** A job's failure when the site has no email design writer. */\nexport const AI_EMAIL_UNAVAILABLE_COPY = 'Email is not available on this site.'\n\n/** A job's failure when a design that passed every check could not be stored. */\nexport const AI_EMAIL_SAVE_FAILURE_COPY =\n 'The email was written but could not be saved. Try the job again.'\n\n/**\n * The kinds of email a job may carry, as `inputs.emailType`, and how the\n * prompt says each.\n *\n * A member picks one for an email of their own; a job the platform composes\n * sets one for itself, which is what `reply` is for (AGL-2918, named by the\n * site model's `AI_SITE_EMAIL_TYPE`). A scaffold's email answers one person\n * who wrote in, and none of the five campaign kinds says that: the nearest,\n * `welcome`, names a signup the site has not had, and a kind that\n * misdescribes the email is worse than no kind at all, because this line is\n * what the model reads first.\n *\n * Every label is the step's own words, so none of them may name a list, a\n * recipient or the people an email is sent to — the same rule the step's\n * instructions keep, held over the whole catalog by `ai-job-email-step.spec`.\n */\nexport const AI_EMAIL_TYPES = {\n welcome: 'a welcome email for someone who just signed up',\n newsletter: 'a newsletter',\n launch: 'a launch announcement',\n abandonedCart: 'a reminder about items left in a cart',\n eventReminder: 'a reminder about an upcoming event',\n reply: 'a reply to somebody who got in touch through the site',\n} as const\n\nexport type AiEmailType = keyof typeof AI_EMAIL_TYPES\n\n/** The merge tokens a generated email's copy may carry: the recipient's own, which each send fills. */\nexport const AI_EMAIL_TEXT_MERGE_TOKENS: readonly string[] = [\n 'contact.firstName',\n 'contact.name',\n 'contact.email',\n]\n\n/** The token the send fills with the site's address, set in front of a link to one of its pages. */\nexport const AI_EMAIL_SITE_URL_TOKEN = '{{site.url}}'\n\n/** The email step's own instructions, cached after the doctrine. */\nexport const AI_JOB_EMAIL_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You write one marketing email for the owner of a website: its design as one flat node map, three subject lines and three preheaders. Answer with submit_email.',\n 'The root is the document wrapper (div, id \"_@_\") and holds only Email sections (emailSection). Each section is a band holding the email’s blocks from top to bottom: Email text (emailText), Email buttons (emailButton), Email images (emailImage), Email dividers (emailDivider), Email spacers (emailSpacer) and, only when the job binds products, Email product blocks (emailProduct).',\n 'Open with a heading (emailText with variant heading), keep each paragraph short, and give the email one main call to action as a button. Greet the reader by first name with {{contact.firstName}} where a greeting fits. The only merge tokens are {{contact.firstName}}, {{contact.name}} and {{contact.email}}, and only in text: never in a link, a subject line or a preheader.',\n 'Every button links somewhere: a page of the site by its address from the site inventory, such as /menu, which is completed with the site’s domain when the email is sent, or a full https address the brief gives.',\n 'An image has an empty src for the owner to fill from the media library, alt text describing the picture it should show, and a width of 600 or less.',\n 'Place exactly as many Email product blocks as the job binds products, one for each, in the order the brief names them. Each shows its product’s current name, price and picture when the email is sent, so never type a product’s price.',\n 'Leave out an unsubscribe link and a postal address: the platform adds the unsubscribe link to every campaign email. A fact the brief does not give, such as a date or a place, goes in square brackets.',\n `Subject lines: three that take different approaches, such as direct, curious and benefit-led, each at most ${AI_EMAIL_SUBJECT_MAX_CHARS} characters and best under 60, with no merge token. Preheaders: three, each extending the subject line in the same position, at most ${AI_EMAIL_PREHEADER_MAX_CHARS} characters and best between 40 and 110.`,\n ].join('\\n'),\n },\n]\n\nconst DOCTRINE_EMAIL_TOOL = aiDoctrineTreeTool('email')\n\n/** The doctrine's email tree tool, with the subject lines and preheaders beside the tree. */\nexport const AI_JOB_EMAIL_TOOL: AiTool = {\n name: DOCTRINE_EMAIL_TOOL.name,\n description:\n 'Submit the email: its design as one flat node map, three subject lines and three preheaders.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'subjects', 'preheaders'],\n properties: {\n tree: (DOCTRINE_EMAIL_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n subjects: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three subject lines that take different approaches, the strongest first.',\n },\n preheaders: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three preheaders, each extending the subject line in the same position.',\n },\n },\n },\n}\n\n/** How the prompt names the kind of email a member picked; `null` for none or an unknown one. */\nexport function aiEmailTypeLabel(value: unknown): string | null {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(AI_EMAIL_TYPES, value)\n ? AI_EMAIL_TYPES[value as AiEmailType]\n : null\n}\n\n/** A name the member gave the job, on one line and bounded; empty when none. */\nexport function aiJobInputName(job: Pick<AiJob, 'inputs'>): string {\n const raw = job.inputs?.['name']\n return typeof raw === 'string' ? raw.replace(/\\s+/g, ' ').trim().slice(0, 120) : ''\n}\n\n/** The email job as the generation's user turn. */\nexport function aiJobEmailPrompt(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n plan: AiJobPlan | null,\n heading: string | null,\n products: number,\n): string {\n const lines = heading ? [heading] : []\n const type = aiEmailTypeLabel(job.inputs?.['emailType'])\n if (type) lines.push(`Kind of email: ${type}`)\n lines.push(aiJobBriefLine(job), ...aiPlanReferenceLines(plan))\n lines.push(\n products > 0\n ? `Products bound to this email: ${products}. Place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: place no Email product block.',\n )\n return lines.join('\\n')\n}\n\n/** The subject lines and preheaders an answer carries. */\nexport interface AiEmailCopy {\n subjects: string[]\n preheaders: string[]\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nexport function parseAiEmailCopy(answer: Readonly<Record<string, unknown>>): AiEmailCopy {\n const lines = (value: unknown) =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string').map(oneLine)\n : []\n return { subjects: lines(answer['subjects']), preheaders: lines(answer['preheaders']) }\n}\n\n/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */\nexport function aiEmailCopyViolations(copy: AiEmailCopy): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const distinct = (lines: string[]) =>\n new Set(lines.filter(Boolean).map((line) => line.toLowerCase())).size\n if (copy.subjects.length !== AI_EMAIL_VARIANTS || distinct(copy.subjects) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-subjects',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different subject lines.`,\n })\n }\n if (copy.preheaders.length !== AI_EMAIL_VARIANTS || distinct(copy.preheaders) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-preheaders',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different preheaders, one for each subject line.`,\n })\n }\n if (copy.subjects.some((line) => line.length > AI_EMAIL_SUBJECT_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-subject-length',\n message: `A subject line is over ${AI_EMAIL_SUBJECT_MAX_CHARS} characters. Keep each within ${AI_EMAIL_SUBJECT_MAX_CHARS}, and best under 60.`,\n })\n }\n if (copy.preheaders.some((line) => line.length > AI_EMAIL_PREHEADER_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-preheader-length',\n message: `A preheader is over ${AI_EMAIL_PREHEADER_MAX_CHARS} characters. Keep each within ${AI_EMAIL_PREHEADER_MAX_CHARS}.`,\n })\n }\n if ([...copy.subjects, ...copy.preheaders].some((line) => line.includes('{{'))) {\n violations.push({\n rule: null,\n code: 'email-header-token',\n message:\n 'A subject line or preheader carries a merge token, which is not filled there. Write them without {{…}}.',\n })\n }\n return violations\n}\n\nconst MERGE_TOKEN = /\\{\\{\\s*([^}]*?)\\s*\\}\\}/g\n\nfunction visitsOf(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>) {\n return walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n}\n\n/** The design's own rules, on a tree the doctrine admitted. */\nexport function aiEmailTreeViolations(tree: AiValidatedTree, products: number): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const loose = (tree.nodes[tree.rootId]?.nodes ?? []).filter(\n (id) => tree.nodes[id]?.componentId !== 'emailSection',\n )\n if (loose.length) {\n violations.push({\n rule: null,\n code: 'email-loose-block',\n message: 'A block sits outside every Email section. Put each block inside a section.',\n nodeIds: aiModelNodeIds(loose, tree.sourceIds),\n })\n }\n const tokens = new Set<string>()\n const tokenNodes: string[] = []\n const unlinked: string[] = []\n let placed = 0\n for (const { id, node } of visitsOf(tree)) {\n for (const value of Object.values(node.props ?? {})) {\n if (typeof value !== 'string') continue\n for (const match of value.matchAll(MERGE_TOKEN)) {\n if (!AI_EMAIL_TEXT_MERGE_TOKENS.includes(match[1])) {\n tokens.add(`{{${match[1]}}}`)\n tokenNodes.push(id)\n }\n }\n }\n if (node.componentId === 'emailButton' && typeof node.props?.['href'] !== 'string') {\n unlinked.push(id)\n }\n if (node.componentId === 'emailProduct') placed += 1\n }\n if (tokens.size) {\n violations.push({\n rule: null,\n code: 'email-merge-token',\n message: `This email writes ${[...tokens].slice(0, 3).join(', ')}, which a generated email may not use. Use only {{contact.firstName}}, {{contact.name}} or {{contact.email}}, and only in text.`,\n nodeIds: aiModelNodeIds([...new Set(tokenNodes)], tree.sourceIds),\n })\n }\n if (unlinked.length) {\n violations.push({\n rule: null,\n code: 'email-button-link',\n message:\n 'An Email button links nowhere. Link each to a page of the site by its address, such as /menu, or to a full https address the brief gives.',\n nodeIds: aiModelNodeIds(unlinked, tree.sourceIds),\n })\n }\n if (placed !== products) {\n violations.push({\n rule: null,\n code: 'email-product-count',\n message: products\n ? `This email binds ${products} ${products === 1 ? 'product' : 'products'}: place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: remove the Email product blocks.',\n })\n }\n return violations\n}\n\n/**\n * The design as it is stored: a link to one of the site's pages completed with\n * the site's address token, and each product block bound, in document order,\n * to the next product the job binds.\n */\nexport function aiEmailDesignNodes(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n productIds: readonly string[],\n): NodesMap {\n const nodes: NodesMap = { ...tree.nodes }\n let next = 0\n for (const { id, node } of visitsOf(tree)) {\n const props: Record<string, unknown> = { ...(node.props ?? {}) }\n let changed = false\n const href = props['href']\n if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//')) {\n props['href'] = `${AI_EMAIL_SITE_URL_TOKEN}${href}`\n changed = true\n }\n if (node.componentId === 'emailProduct') {\n const productId = productIds[next]\n next += 1\n if (productId) {\n props['productId'] = productId\n changed = true\n }\n }\n if (changed) nodes[id] = { ...nodes[id], props } as NodesMap[string]\n }\n return nodes\n}\n\n/** The content the email plugin's writer stores a design from. */\nexport function aiEmailDesignContent(nodes: NodesMap, copy: AiEmailCopy): Record<string, unknown> {\n return {\n nodes,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** What the email plugin's renderer says about the design, as violations the model is re-asked with. */\nexport function aiEmailRenderViolations(\n design: PluginResourceDraftWriter,\n hostId: string,\n nodes: NodesMap,\n copy: AiEmailCopy,\n): AiDoctrineViolation[] {\n const checked = design.check(aiEmailDesignContent(nodes, copy), { hostId })\n if (checked.ok === true) return []\n return [...new Set(checked.problems)].map((problem) => ({\n rule: null,\n code: 'email-render',\n message: `The email does not render as written: ${problem}.`,\n }))\n}\n\nexport interface AiEmailGenerationInput {\n job: AiJob\n step: Extract<AiStepKind, 'job.email' | 'job.campaign'>\n model: string\n /** The line naming what is built, such as `Email name: …`; `null` for none. */\n heading: string | null\n plan: AiJobPlan | null\n inventory: AiSiteInventory | null\n hostId: string\n products: AiEmailProductBinding\n /** The email plugin's design writer, whose check the design must pass. */\n design: PluginResourceDraftWriter\n signal?: AbortSignal\n}\n\nexport interface AiEmailGeneration {\n result: AiValidatedGeneration<AiValidatedTree>\n /** The copy of the answer the doctrine kept; `null` unless it kept one. */\n copy: AiEmailCopy | null\n /** The kept design as it is stored; `null` unless the doctrine kept one. */\n nodes: NodesMap | null\n}\n\n/** Generates one email under the doctrine, with the email's own checks riding `extend`. */\nexport async function generateAiEmail(input: AiEmailGenerationInput): Promise<AiEmailGeneration> {\n // The answer `extend` saw last is the one the doctrine kept when it keeps one.\n let last = null as { copy: AiEmailCopy; nodes: NodesMap } | null\n const result = await runValidatedGeneration('email', {\n step: input.step,\n model: input.model,\n instructions: AI_JOB_EMAIL_INSTRUCTIONS,\n inventory: input.inventory,\n messages: [\n {\n role: 'user',\n content: aiJobEmailPrompt(input.job, input.plan, input.heading, input.products.ids.length),\n },\n ],\n tool: AI_JOB_EMAIL_TOOL,\n maxTokens: AI_JOB_EMAIL_STEP_BUDGETS[input.step].maxTokens(input.model),\n thinking: 'off',\n extend: (tree, answer) => {\n const copy = parseAiEmailCopy(answer)\n const nodes = aiEmailDesignNodes(tree, input.products.ids)\n last = { copy, nodes }\n const violations = [\n ...aiEmailTreeViolations(tree, input.products.ids.length),\n ...aiEmailCopyViolations(copy),\n ]\n // Rendered once the design's own rules hold, so a problem is named once.\n return violations.length\n ? violations\n : aiEmailRenderViolations(input.design, input.hostId, nodes, copy)\n },\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const kept = result.status === 'ok' ? last : null\n return { result, copy: kept?.copy ?? null, nodes: kept?.nodes ?? null }\n}\n\n/** An email design as the job's output. */\nexport function aiEmailDesignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'emailScreen',\n id: record.id,\n versionId: record.versionId,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/** An email job is admitted for a site of its org with email on and room for a design. */\nexport const aiEmailJobAdmission: AiJobAdmission = (context) =>\n aiPluginDraftAdmissionRefusal(context, {\n kind: 'email',\n drafts: [{ resource: AI_EMAIL_DESIGN_RESOURCE, label: 'Email' }],\n })\n\nexport interface AiJobEmailStepDeps {\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\nexport function createAiJobEmailStep(deps: AiJobEmailStepDeps = {}): 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('an email 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.email') ?? aiModelForStep('job.email')\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n if (!design) return { ...aiUnspentOutcome(model), failure: AI_EMAIL_UNAVAILABLE_COPY }\n\n // A run cut off after its design was written reports that design, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'email')\n const written = await design.read({ hostId, id: draftId })\n if (written) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [aiEmailDesignOutput(written, { hostId, hostSubdomain }, null)],\n })\n }\n\n const [inventory, orgSnapshot, products, hostSubdomain] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n resolveAiEmailProducts({ hostId, brief: job.brief, inputs: job.inputs }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\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 plan = aiConfirmedPlan(job)\n const name =\n aiPlanCreation(plan, 'email')?.name || aiJobInputName(job) || AI_JOB_EMAIL_DEFAULT_NAME\n const generated = await generateAiEmail({\n job,\n step: 'job.email',\n model,\n heading: `Email name: ${name}`,\n plan,\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 write = await design.write({\n ...context,\n id: draftId,\n name,\n content: aiEmailDesignContent(generated.nodes, generated.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 {\n ...spent,\n outputs: [aiEmailDesignOutput(write, { hostId, hostSubdomain }, aiEmailProductNote(products))],\n }\n }\n}\n\nexport const runAiJobEmailStep = createAiJobEmailStep()\n\n/** Registers the email step and the check a email job passes before it is created or resumed. */\nexport function registerAiEmailJob(): void {\n registerAiJobStep('email', runAiJobEmailStep, { minimumMs: AI_JOB_EMAIL_STEP_MINIMUM_MS })\n registerAiJobAdmission('email', aiEmailJobAdmission)\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_TEXT_LIMITS","readSiteInventory","aiEmailProductNote","resolveAiEmailProducts","registerAiJobAdmission","aiJobStepBudget","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_EMAIL_DESIGN_RESOURCE","AI_JOB_EMAIL_MAX_TOKENS","maxTokens","AI_JOB_EMAIL_STEP_BUDGETS","tier","AI_JOB_EMAIL_STEP_MINIMUM_MS","minimumMs","AI_EMAIL_VARIANTS","AI_EMAIL_SUBJECT_MAX_CHARS","headline","AI_EMAIL_PREHEADER_MAX_CHARS","AI_JOB_EMAIL_DEFAULT_NAME","AI_EMAIL_UNAVAILABLE_COPY","AI_EMAIL_SAVE_FAILURE_COPY","AI_EMAIL_TYPES","welcome","newsletter","launch","abandonedCart","eventReminder","reply","AI_EMAIL_TEXT_MERGE_TOKENS","AI_EMAIL_SITE_URL_TOKEN","AI_JOB_EMAIL_INSTRUCTIONS","text","join","DOCTRINE_EMAIL_TOOL","AI_JOB_EMAIL_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","subjects","items","preheaders","aiEmailTypeLabel","value","Object","prototype","hasOwnProperty","call","aiJobInputName","job","raw","inputs","replace","trim","slice","aiJobEmailPrompt","plan","heading","products","lines","push","oneLine","parseAiEmailCopy","answer","Array","isArray","filter","entry","map","aiEmailCopyViolations","copy","violations","distinct","Set","Boolean","line","toLowerCase","size","length","rule","code","message","some","includes","MERGE_TOKEN","visitsOf","rootId","nodes","aiEmailTreeViolations","loose","id","componentId","nodeIds","sourceIds","tokens","tokenNodes","unlinked","placed","node","values","props","match","matchAll","add","aiEmailDesignNodes","productIds","next","changed","href","startsWith","productId","aiEmailDesignContent","subject","preheader","subjectVariants","preheaderVariants","aiEmailRenderViolations","design","hostId","checked","check","ok","problems","problem","generateAiEmail","input","last","result","step","model","instructions","inventory","messages","role","content","ids","tool","thinking","extend","signal","kept","status","aiEmailDesignOutput","record","place","note","resource","versionId","hostSubdomain","label","aiEmailJobAdmission","context","kind","drafts","createAiJobEmailStep","deps","readInventory","writerFor","now","firestore","modelFor","orgSnapshot","Error","failure","draftId","written","read","outputs","Promise","all","orgId","collection","doc","get","brief","org","data","uid","createdBy","refusal","review","error","generated","spent","refused","write","runAiJobEmailStep","registerAiEmailJob"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-email-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 {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport { AI_TEXT_LIMITS } from '../runtime/ai-palette'\nimport type { AiSystemBlock, AiTool } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiEmailProductNote,\n resolveAiEmailProducts,\n type AiEmailProductBinding,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobPlanCreationsRefusal } from './ai-job-plan-creations'\nimport { aiJobStepBudget, type AiJobStepBudget } from './ai-job-budget'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\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 email step (AGL-2912): the generation step of an `email` job, run once a\n * member confirmed the job's plan, and the generation the campaign step\n * drafts its email with.\n *\n * It builds ONE email design from the email palette — sections holding text,\n * buttons, images, dividers, spacers and product blocks, never hand-written\n * HTML, which is not on the email surface — through\n * `runValidatedGeneration('email', …)`, so every building rule holds on the\n * tree, rule 6's brand and rule 17's clip size included. Beside the design the\n * answer carries three subject lines and three preheaders. The door's checks\n * ride `extend`:\n *\n * - every block sits in a section, every button links somewhere, and the\n * copy's only merge tokens are the recipient's own, in text;\n * - exactly as many product blocks as the job binds products;\n * - three different subject lines and preheaders, within their lengths and\n * free of merge tokens, which a subject line does not fill;\n * - and the design RENDERS: the email plugin's writer checks it through the\n * send path's own renderer, reached through the core's resource-drafts\n * seam, and a problem it names is a violation the model is re-asked with.\n *\n * What the model is sent is the brief, the email's name and kind, the\n * confirmed plan and the site inventory, and how many product blocks to place.\n * The products themselves are bound by id after it answers\n * (`ai-email-bindings.ts`); a link to one of the site's pages is completed\n * with the site's address token, which the send fills.\n *\n * The design lands through that same writer: a draft email screen, listed on\n * the Emails page's templates, that no campaign sends until a member picks it.\n * The job never sends, schedules or publishes anything.\n *\n * Like the other generation steps, it runs on the job beat, never at an inline\n * door: it registers the least time its lookup rounds, its answer and its\n * re-ask need (AGL-3035), and runs without extended thinking under a tighter\n * answer ceiling than the doctrine's default for an email, lowered on a tier\n * too slow to answer it inside that time.\n */\n\n/** The resource the email plugin writes an email design under. */\nexport const AI_EMAIL_DESIGN_RESOURCE = 'emailDesign'\n\n/** The longest answer an email may run to; the tree is held to the email budget either way. */\nexport const AI_JOB_EMAIL_MAX_TOKENS = AI_ROUTING_TABLE['job.email'].maxTokens\n\n/**\n * The time an email generation takes (AGL-3035, AGL-3036), for each step kind\n * that runs one: its two inventory-lookup rounds, its answer and its re-ask at\n * the kind's routing ceiling on the tier the kind is served from, fitted to\n * what a beat can give a step. The email step registers the first, and the\n * campaign step — whose email is this same generation — the second.\n */\nexport const AI_JOB_EMAIL_STEP_BUDGETS: Readonly<\n Record<AiEmailGenerationInput['step'], AiJobStepBudget>\n> = {\n 'job.email': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.email'],\n maxTokens: AI_ROUTING_TABLE['job.email'].maxTokens,\n }),\n 'job.campaign': aiJobStepBudget({\n tier: AI_STEP_TIERS['job.campaign'],\n maxTokens: AI_ROUTING_TABLE['job.campaign'].maxTokens,\n }),\n}\n\n/** The least time one email step needs before it starts. */\nexport const AI_JOB_EMAIL_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.email'].minimumMs\n\n/** How many subject lines, and how many preheaders, an email is written with. */\nexport const AI_EMAIL_VARIANTS = 3\n\n/** A subject line is a headline. */\nexport const AI_EMAIL_SUBJECT_MAX_CHARS = AI_TEXT_LIMITS.headline\n\n/** The longest preheader the campaign send route stores. */\nexport const AI_EMAIL_PREHEADER_MAX_CHARS = 200\n\n/** An email's name when neither the plan nor the job names one. */\nexport const AI_JOB_EMAIL_DEFAULT_NAME = 'New email'\n\n/** A job's failure when the site has no email design writer. */\nexport const AI_EMAIL_UNAVAILABLE_COPY = 'Email is not available on this site.'\n\n/** A job's failure when a design that passed every check could not be stored. */\nexport const AI_EMAIL_SAVE_FAILURE_COPY =\n 'The email was written but could not be saved. Try the job again.'\n\n/**\n * The kinds of email a job may carry, as `inputs.emailType`, and how the\n * prompt says each.\n *\n * A member picks one for an email of their own; a job the platform composes\n * sets one for itself, which is what `reply` is for (AGL-2918, named by the\n * site model's `AI_SITE_EMAIL_TYPE`). A scaffold's email answers one person\n * who wrote in, and none of the five campaign kinds says that: the nearest,\n * `welcome`, names a signup the site has not had, and a kind that\n * misdescribes the email is worse than no kind at all, because this line is\n * what the model reads first.\n *\n * Every label is the step's own words, so none of them may name a list, a\n * recipient or the people an email is sent to — the same rule the step's\n * instructions keep, held over the whole catalog by `ai-job-email-step.spec`.\n */\nexport const AI_EMAIL_TYPES = {\n welcome: 'a welcome email for someone who just signed up',\n newsletter: 'a newsletter',\n launch: 'a launch announcement',\n abandonedCart: 'a reminder about items left in a cart',\n eventReminder: 'a reminder about an upcoming event',\n reply: 'a reply to somebody who got in touch through the site',\n} as const\n\nexport type AiEmailType = keyof typeof AI_EMAIL_TYPES\n\n/** The merge tokens a generated email's copy may carry: the recipient's own, which each send fills. */\nexport const AI_EMAIL_TEXT_MERGE_TOKENS: readonly string[] = [\n 'contact.firstName',\n 'contact.name',\n 'contact.email',\n]\n\n/** The token the send fills with the site's address, set in front of a link to one of its pages. */\nexport const AI_EMAIL_SITE_URL_TOKEN = '{{site.url}}'\n\n/** The email step's own instructions, cached after the doctrine. */\nexport const AI_JOB_EMAIL_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You write one marketing email for the owner of a website: its design as one flat node map, three subject lines and three preheaders. Answer with submit_email.',\n 'The root is the document wrapper (div, id \"_@_\") and holds only Email sections (emailSection). Each section is a band holding the email’s blocks from top to bottom: Email text (emailText), Email buttons (emailButton), Email images (emailImage), Email dividers (emailDivider), Email spacers (emailSpacer) and, only when the job binds products, Email product blocks (emailProduct).',\n 'Open with a heading (emailText with variant heading), keep each paragraph short, and give the email one main call to action as a button. Greet the reader by first name with {{contact.firstName}} where a greeting fits. The only merge tokens are {{contact.firstName}}, {{contact.name}} and {{contact.email}}, and only in text: never in a link, a subject line or a preheader.',\n 'Every button links somewhere: a page of the site by its address from the site inventory, such as /menu, which is completed with the site’s domain when the email is sent, or a full https address the brief gives.',\n 'An image has an empty src for the owner to fill from the media library, alt text describing the picture it should show, and a width of 600 or less.',\n 'Place exactly as many Email product blocks as the job binds products, one for each, in the order the brief names them. Each shows its product’s current name, price and picture when the email is sent, so never type a product’s price.',\n 'Leave out an unsubscribe link and a postal address: the platform adds the unsubscribe link to every campaign email. A fact the brief does not give, such as a date or a place, goes in square brackets.',\n `Subject lines: three that take different approaches, such as direct, curious and benefit-led, each at most ${AI_EMAIL_SUBJECT_MAX_CHARS} characters and best under 60, with no merge token. Preheaders: three, each extending the subject line in the same position, at most ${AI_EMAIL_PREHEADER_MAX_CHARS} characters and best between 40 and 110.`,\n ].join('\\n'),\n },\n]\n\nconst DOCTRINE_EMAIL_TOOL = aiDoctrineTreeTool('email')\n\n/** The doctrine's email tree tool, with the subject lines and preheaders beside the tree. */\nexport const AI_JOB_EMAIL_TOOL: AiTool = {\n name: DOCTRINE_EMAIL_TOOL.name,\n description:\n 'Submit the email: its design as one flat node map, three subject lines and three preheaders.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['tree', 'subjects', 'preheaders'],\n properties: {\n tree: (DOCTRINE_EMAIL_TOOL.inputSchema['properties'] as Record<string, unknown>)['tree'],\n subjects: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three subject lines that take different approaches, the strongest first.',\n },\n preheaders: {\n type: 'array',\n items: { type: 'string' },\n description: 'Three preheaders, each extending the subject line in the same position.',\n },\n },\n },\n}\n\n/** How the prompt names the kind of email a member picked; `null` for none or an unknown one. */\nexport function aiEmailTypeLabel(value: unknown): string | null {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(AI_EMAIL_TYPES, value)\n ? AI_EMAIL_TYPES[value as AiEmailType]\n : null\n}\n\n/** A name the member gave the job, on one line and bounded; empty when none. */\nexport function aiJobInputName(job: Pick<AiJob, 'inputs'>): string {\n const raw = job.inputs?.['name']\n return typeof raw === 'string' ? raw.replace(/\\s+/g, ' ').trim().slice(0, 120) : ''\n}\n\n/** The email job as the generation's user turn. */\nexport function aiJobEmailPrompt(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n plan: AiJobPlan | null,\n heading: string | null,\n products: number,\n): string {\n const lines = heading ? [heading] : []\n const type = aiEmailTypeLabel(job.inputs?.['emailType'])\n if (type) lines.push(`Kind of email: ${type}`)\n lines.push(aiJobBriefLine(job), ...aiPlanReferenceLines(plan))\n lines.push(\n products > 0\n ? `Products bound to this email: ${products}. Place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: place no Email product block.',\n )\n return lines.join('\\n')\n}\n\n/** The subject lines and preheaders an answer carries. */\nexport interface AiEmailCopy {\n subjects: string[]\n preheaders: string[]\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim()\n}\n\nexport function parseAiEmailCopy(answer: Readonly<Record<string, unknown>>): AiEmailCopy {\n const lines = (value: unknown) =>\n Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === 'string').map(oneLine)\n : []\n return { subjects: lines(answer['subjects']), preheaders: lines(answer['preheaders']) }\n}\n\n/** The copy's own rules: three different lines of each, within their lengths, with no merge token. */\nexport function aiEmailCopyViolations(copy: AiEmailCopy): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const distinct = (lines: string[]) =>\n new Set(lines.filter(Boolean).map((line) => line.toLowerCase())).size\n if (copy.subjects.length !== AI_EMAIL_VARIANTS || distinct(copy.subjects) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-subjects',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different subject lines.`,\n })\n }\n if (copy.preheaders.length !== AI_EMAIL_VARIANTS || distinct(copy.preheaders) !== AI_EMAIL_VARIANTS) {\n violations.push({\n rule: null,\n code: 'email-preheaders',\n message: `Write exactly ${AI_EMAIL_VARIANTS} different preheaders, one for each subject line.`,\n })\n }\n if (copy.subjects.some((line) => line.length > AI_EMAIL_SUBJECT_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-subject-length',\n message: `A subject line is over ${AI_EMAIL_SUBJECT_MAX_CHARS} characters. Keep each within ${AI_EMAIL_SUBJECT_MAX_CHARS}, and best under 60.`,\n })\n }\n if (copy.preheaders.some((line) => line.length > AI_EMAIL_PREHEADER_MAX_CHARS)) {\n violations.push({\n rule: null,\n code: 'email-preheader-length',\n message: `A preheader is over ${AI_EMAIL_PREHEADER_MAX_CHARS} characters. Keep each within ${AI_EMAIL_PREHEADER_MAX_CHARS}.`,\n })\n }\n if ([...copy.subjects, ...copy.preheaders].some((line) => line.includes('{{'))) {\n violations.push({\n rule: null,\n code: 'email-header-token',\n message:\n 'A subject line or preheader carries a merge token, which is not filled there. Write them without {{…}}.',\n })\n }\n return violations\n}\n\nconst MERGE_TOKEN = /\\{\\{\\s*([^}]*?)\\s*\\}\\}/g\n\nfunction visitsOf(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>) {\n return walkTree({\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n })\n}\n\n/** The design's own rules, on a tree the doctrine admitted. */\nexport function aiEmailTreeViolations(tree: AiValidatedTree, products: number): AiDoctrineViolation[] {\n const violations: AiDoctrineViolation[] = []\n const loose = (tree.nodes[tree.rootId]?.nodes ?? []).filter(\n (id) => tree.nodes[id]?.componentId !== 'emailSection',\n )\n if (loose.length) {\n violations.push({\n rule: null,\n code: 'email-loose-block',\n message: 'A block sits outside every Email section. Put each block inside a section.',\n nodeIds: aiModelNodeIds(loose, tree.sourceIds),\n })\n }\n const tokens = new Set<string>()\n const tokenNodes: string[] = []\n const unlinked: string[] = []\n let placed = 0\n for (const { id, node } of visitsOf(tree)) {\n for (const value of Object.values(node.props ?? {})) {\n if (typeof value !== 'string') continue\n for (const match of value.matchAll(MERGE_TOKEN)) {\n if (!AI_EMAIL_TEXT_MERGE_TOKENS.includes(match[1])) {\n tokens.add(`{{${match[1]}}}`)\n tokenNodes.push(id)\n }\n }\n }\n if (node.componentId === 'emailButton' && typeof node.props?.['href'] !== 'string') {\n unlinked.push(id)\n }\n if (node.componentId === 'emailProduct') placed += 1\n }\n if (tokens.size) {\n violations.push({\n rule: null,\n code: 'email-merge-token',\n message: `This email writes ${[...tokens].slice(0, 3).join(', ')}, which a generated email may not use. Use only {{contact.firstName}}, {{contact.name}} or {{contact.email}}, and only in text.`,\n nodeIds: aiModelNodeIds([...new Set(tokenNodes)], tree.sourceIds),\n })\n }\n if (unlinked.length) {\n violations.push({\n rule: null,\n code: 'email-button-link',\n message:\n 'An Email button links nowhere. Link each to a page of the site by its address, such as /menu, or to a full https address the brief gives.',\n nodeIds: aiModelNodeIds(unlinked, tree.sourceIds),\n })\n }\n if (placed !== products) {\n violations.push({\n rule: null,\n code: 'email-product-count',\n message: products\n ? `This email binds ${products} ${products === 1 ? 'product' : 'products'}: place exactly ${products} Email product ${\n products === 1 ? 'block' : 'blocks'\n }.`\n : 'No products are bound to this email: remove the Email product blocks.',\n })\n }\n return violations\n}\n\n/**\n * The design as it is stored: a link to one of the site's pages completed with\n * the site's address token, and each product block bound, in document order,\n * to the next product the job binds.\n */\nexport function aiEmailDesignNodes(\n tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>,\n productIds: readonly string[],\n): NodesMap {\n const nodes: NodesMap = { ...tree.nodes }\n let next = 0\n for (const { id, node } of visitsOf(tree)) {\n const props: Record<string, unknown> = { ...(node.props ?? {}) }\n let changed = false\n const href = props['href']\n if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//')) {\n props['href'] = `${AI_EMAIL_SITE_URL_TOKEN}${href}`\n changed = true\n }\n if (node.componentId === 'emailProduct') {\n const productId = productIds[next]\n next += 1\n if (productId) {\n props['productId'] = productId\n changed = true\n }\n }\n if (changed) nodes[id] = { ...nodes[id], props } as NodesMap[string]\n }\n return nodes\n}\n\n/** The content the email plugin's writer stores a design from. */\nexport function aiEmailDesignContent(nodes: NodesMap, copy: AiEmailCopy): Record<string, unknown> {\n return {\n nodes,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** What the email plugin's renderer says about the design, as violations the model is re-asked with. */\nexport function aiEmailRenderViolations(\n design: PluginResourceDraftWriter,\n hostId: string,\n nodes: NodesMap,\n copy: AiEmailCopy,\n): AiDoctrineViolation[] {\n const checked = design.check(aiEmailDesignContent(nodes, copy), { hostId })\n if (checked.ok === true) return []\n return [...new Set(checked.problems)].map((problem) => ({\n rule: null,\n code: 'email-render',\n message: `The email does not render as written: ${problem}.`,\n }))\n}\n\nexport interface AiEmailGenerationInput {\n job: AiJob\n step: Extract<AiStepKind, 'job.email' | 'job.campaign'>\n model: string\n /** The line naming what is built, such as `Email name: …`; `null` for none. */\n heading: string | null\n plan: AiJobPlan | null\n inventory: AiSiteInventory | null\n hostId: string\n products: AiEmailProductBinding\n /** The email plugin's design writer, whose check the design must pass. */\n design: PluginResourceDraftWriter\n signal?: AbortSignal\n}\n\nexport interface AiEmailGeneration {\n result: AiValidatedGeneration<AiValidatedTree>\n /** The copy of the answer the doctrine kept; `null` unless it kept one. */\n copy: AiEmailCopy | null\n /** The kept design as it is stored; `null` unless the doctrine kept one. */\n nodes: NodesMap | null\n}\n\n/** Generates one email under the doctrine, with the email's own checks riding `extend`. */\nexport async function generateAiEmail(input: AiEmailGenerationInput): Promise<AiEmailGeneration> {\n // The answer `extend` saw last is the one the doctrine kept when it keeps one.\n let last = null as { copy: AiEmailCopy; nodes: NodesMap } | null\n const result = await runValidatedGeneration('email', {\n step: input.step,\n model: input.model,\n instructions: AI_JOB_EMAIL_INSTRUCTIONS,\n inventory: input.inventory,\n messages: [\n {\n role: 'user',\n content: aiJobEmailPrompt(input.job, input.plan, input.heading, input.products.ids.length),\n },\n ],\n tool: AI_JOB_EMAIL_TOOL,\n maxTokens: AI_JOB_EMAIL_STEP_BUDGETS[input.step].maxTokens(input.model),\n thinking: 'off',\n extend: (tree, answer) => {\n const copy = parseAiEmailCopy(answer)\n const nodes = aiEmailDesignNodes(tree, input.products.ids)\n last = { copy, nodes }\n const violations = [\n ...aiEmailTreeViolations(tree, input.products.ids.length),\n ...aiEmailCopyViolations(copy),\n ]\n // Rendered once the design's own rules hold, so a problem is named once.\n return violations.length\n ? violations\n : aiEmailRenderViolations(input.design, input.hostId, nodes, copy)\n },\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const kept = result.status === 'ok' ? last : null\n return { result, copy: kept?.copy ?? null, nodes: kept?.nodes ?? null }\n}\n\n/** An email design as the job's output. */\nexport function aiEmailDesignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'emailScreen',\n id: record.id,\n versionId: record.versionId,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/**\n * An email job is admitted for a site of its org with email on and room for a\n * design, and confirmed only for a plan that creates nothing but the design:\n * an email places nothing another job builds (AGL-3143 §15).\n */\nexport const aiEmailJobAdmission: AiJobAdmission = async (context) => {\n const refusal = await aiPluginDraftAdmissionRefusal(context, {\n kind: 'email',\n drafts: [{ resource: AI_EMAIL_DESIGN_RESOURCE, label: 'Email' }],\n })\n if (refusal || !context.hostId) return refusal\n return aiJobPlanCreationsRefusal('email', context, context.hostId)\n}\n\nexport interface AiJobEmailStepDeps {\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\nexport function createAiJobEmailStep(deps: AiJobEmailStepDeps = {}): 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('an email 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.email') ?? aiModelForStep('job.email')\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n if (!design) return { ...aiUnspentOutcome(model), failure: AI_EMAIL_UNAVAILABLE_COPY }\n\n // A run cut off after its design was written reports that design, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'email')\n const written = await design.read({ hostId, id: draftId })\n if (written) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [aiEmailDesignOutput(written, { hostId, hostSubdomain }, null)],\n })\n }\n\n const [inventory, orgSnapshot, products, hostSubdomain] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n resolveAiEmailProducts({ hostId, brief: job.brief, inputs: job.inputs }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\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 plan = aiConfirmedPlan(job)\n const name =\n aiPlanCreation(plan, 'email')?.name || aiJobInputName(job) || AI_JOB_EMAIL_DEFAULT_NAME\n const generated = await generateAiEmail({\n job,\n step: 'job.email',\n model,\n heading: `Email name: ${name}`,\n plan,\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 write = await design.write({\n ...context,\n id: draftId,\n name,\n content: aiEmailDesignContent(generated.nodes, generated.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 {\n ...spent,\n outputs: [aiEmailDesignOutput(write, { hostId, hostSubdomain }, aiEmailProductNote(products))],\n }\n }\n}\n\nexport const runAiJobEmailStep = createAiJobEmailStep()\n\n/** Registers the email step and the check a email job passes before it is created or resumed. */\nexport function registerAiEmailJob(): void {\n registerAiJobStep('email', runAiJobEmailStep, { minimumMs: AI_JOB_EMAIL_STEP_MINIMUM_MS })\n registerAiJobAdmission('email', aiEmailJobAdmission)\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_TEXT_LIMITS","readSiteInventory","aiEmailProductNote","resolveAiEmailProducts","registerAiJobAdmission","aiJobPlanCreationsRefusal","aiJobStepBudget","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_EMAIL_DESIGN_RESOURCE","AI_JOB_EMAIL_MAX_TOKENS","maxTokens","AI_JOB_EMAIL_STEP_BUDGETS","tier","AI_JOB_EMAIL_STEP_MINIMUM_MS","minimumMs","AI_EMAIL_VARIANTS","AI_EMAIL_SUBJECT_MAX_CHARS","headline","AI_EMAIL_PREHEADER_MAX_CHARS","AI_JOB_EMAIL_DEFAULT_NAME","AI_EMAIL_UNAVAILABLE_COPY","AI_EMAIL_SAVE_FAILURE_COPY","AI_EMAIL_TYPES","welcome","newsletter","launch","abandonedCart","eventReminder","reply","AI_EMAIL_TEXT_MERGE_TOKENS","AI_EMAIL_SITE_URL_TOKEN","AI_JOB_EMAIL_INSTRUCTIONS","text","join","DOCTRINE_EMAIL_TOOL","AI_JOB_EMAIL_TOOL","name","description","strict","inputSchema","type","additionalProperties","required","properties","tree","subjects","items","preheaders","aiEmailTypeLabel","value","Object","prototype","hasOwnProperty","call","aiJobInputName","job","raw","inputs","replace","trim","slice","aiJobEmailPrompt","plan","heading","products","lines","push","oneLine","parseAiEmailCopy","answer","Array","isArray","filter","entry","map","aiEmailCopyViolations","copy","violations","distinct","Set","Boolean","line","toLowerCase","size","length","rule","code","message","some","includes","MERGE_TOKEN","visitsOf","rootId","nodes","aiEmailTreeViolations","loose","id","componentId","nodeIds","sourceIds","tokens","tokenNodes","unlinked","placed","node","values","props","match","matchAll","add","aiEmailDesignNodes","productIds","next","changed","href","startsWith","productId","aiEmailDesignContent","subject","preheader","subjectVariants","preheaderVariants","aiEmailRenderViolations","design","hostId","checked","check","ok","problems","problem","generateAiEmail","input","last","result","step","model","instructions","inventory","messages","role","content","ids","tool","thinking","extend","signal","kept","status","aiEmailDesignOutput","record","place","note","resource","versionId","hostSubdomain","label","aiEmailJobAdmission","context","refusal","kind","drafts","createAiJobEmailStep","deps","readInventory","writerFor","now","firestore","modelFor","orgSnapshot","Error","failure","draftId","written","read","outputs","Promise","all","orgId","collection","doc","get","brief","org","data","uid","createdBy","review","error","generated","spent","refused","write","runAiJobEmailStep","registerAiEmailJob"],"mappings":"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"}
@@ -231,6 +231,17 @@ export declare function aiCopiedFormReview(firestore: FirebaseFirestore.Firestor
231
231
  name: string;
232
232
  plan: AiJobPlan | null;
233
233
  }): Promise<AiJobReview | null>;
234
+ /**
235
+ * What the form a short copy would have started from collects, as the build
236
+ * that replaces the copy is told it (AGL-3143 §14): every field it asks for,
237
+ * so the build keeps them beside what the plan adds. Empty when the form
238
+ * cannot be read.
239
+ */
240
+ export declare function aiFormSourceLines(firestore: FirebaseFirestore.Firestore, input: {
241
+ hostId: string;
242
+ id: string;
243
+ name: string;
244
+ }): Promise<string[]>;
234
245
  /**
235
246
  * What the person decides next, in words the step writes itself: a lead
236
247
  * routing to keep or switch off, the list a proposal could not store, and what
@@ -257,7 +268,12 @@ export declare function aiJobFormPrompt(job: Pick<AiJob, 'brief'>, plan: AiJobPl
257
268
  audience?: string;
258
269
  submissions?: AiSiteSubmissions | null;
259
270
  }): string;
260
- /** A form job is admitted for a site of the job's own org whose plan has forms, with a form to spare. */
271
+ /**
272
+ * A form job is admitted for a site of the job's own org whose plan has
273
+ * forms, with a form to spare, and confirmed only for a plan that creates
274
+ * nothing but the form: a form places nothing another job builds
275
+ * (AGL-3143 §15).
276
+ */
261
277
  export declare const aiFormJobAdmission: AiJobAdmission;
262
278
  export interface AiJobFormStepDeps {
263
279
  /** The inventory reader; specs hand in a fake. */
@@ -27,6 +27,7 @@ import { aiDoctrineTreeTool, runValidatedGeneration } from "../runtime/ai-doctri
27
27
  import { AI_PALETTE } from "../runtime/ai-palette.generated.js";
28
28
  import { readSiteInventory } from "../runtime/site-inventory.js";
29
29
  import { registerAiJobAdmission } from "./ai-job-admission.js";
30
+ import { aiJobPlanCreationsRefusal } from "./ai-job-plan-creations.js";
30
31
  import { aiJobDraftId } from "./ai-job-draft-ids.js";
31
32
  import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, writeAiDraft } from "./ai-job-drafts.js";
32
33
  import { aiBracketedFactsNote, aiConfirmedPlan, aiDoctrineReview, aiGenerationSpent, aiJobBriefLine, aiLimitReview, aiModelNodeIds, aiPlanCreation, aiPlanReferenceLines, aiUnspentOutcome } from "./ai-job-generation.js";
@@ -439,6 +440,32 @@ function oneLine(value, max) {
439
440
  ]
440
441
  });
441
442
  }
443
+ /**
444
+ * What the form a short copy would have started from collects, as the build
445
+ * that replaces the copy is told it (AGL-3143 §14): every field it asks for,
446
+ * so the build keeps them beside what the plan adds. Empty when the form
447
+ * cannot be read.
448
+ */ export async function aiFormSourceLines(firestore, input) {
449
+ var _snapshot_get;
450
+ const snapshot = await firestore.collection('hosts').doc(input.hostId).collection('forms').doc(input.id).get();
451
+ const fields = snapshot.exists ? (_snapshot_get = snapshot.get('fields')) != null ? _snapshot_get : [] : [];
452
+ const asked = fields.flatMap((field)=>{
453
+ var _ref, _row_fieldName, _row_fieldType;
454
+ if (!field || typeof field !== 'object') return [];
455
+ const row = field;
456
+ const name = String((_ref = (_row_fieldName = row['fieldName']) != null ? _row_fieldName : row['name']) != null ? _ref : '').trim();
457
+ if (!name) return [];
458
+ const fieldType = (_row_fieldType = row['fieldType']) != null ? _row_fieldType : row['type'];
459
+ const type = typeof fieldType === 'string' ? ` (${fieldType}${row['required'] ? ', required' : ''})` : '';
460
+ return [
461
+ `${name}${type}`
462
+ ];
463
+ });
464
+ if (!asked.length) return [];
465
+ return [
466
+ `The plan starts from a copy of the form "${input.name}", which lacks part of what the plan gives it. Build from it: it collects ${asked.join(', ')}. Keep each of those, and add what the plan adds.`
467
+ ];
468
+ }
442
469
  function listOf(items) {
443
470
  return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
444
471
  }
@@ -490,11 +517,17 @@ function listOf(items) {
490
517
  inbox: 'the Inbox, to be read — routing.kind is inbox',
491
518
  lead: 'the Inbox, and each one with an email address is a sales lead — routing.kind is lead'
492
519
  };
493
- /** A form job is admitted for a site of the job's own org whose plan has forms, with a form to spare. */ export const aiFormJobAdmission = (context)=>aiDraftAdmissionRefusal(context.firestore, {
520
+ /**
521
+ * A form job is admitted for a site of the job's own org whose plan has
522
+ * forms, with a form to spare, and confirmed only for a plan that creates
523
+ * nothing but the form: a form places nothing another job builds
524
+ * (AGL-3143 §15).
525
+ */ export const aiFormJobAdmission = (context)=>aiDraftAdmissionRefusal(context.firestore, {
494
526
  orgId: context.orgId,
495
527
  hostId: context.hostId,
496
528
  kind: 'form',
497
- org: context.org
529
+ org: context.org,
530
+ ownCheck: (hostId)=>aiJobPlanCreationsRefusal('form', context, hostId)
498
531
  });
499
532
  export function createAiJobFormStep(deps = {}) {
500
533
  var _deps_readInventory, _deps_duplicate;
@@ -542,8 +575,24 @@ export function createAiJobFormStep(deps = {}) {
542
575
  const name = (creation == null ? void 0 : creation.name) || AI_JOB_FORM_DEFAULT_NAME;
543
576
  // The plan starts from a copy of a form the site has (rule 15): the copy
544
577
  // is the draft, and nothing is generated. A source gone since the plan
545
- // was made is built from the brief instead.
546
- if ((creation == null ? void 0 : creation.duplicateOf) && inventory.forms.some((form)=>form.id === creation.duplicateOf)) {
578
+ // was made is built from the brief instead, and one that does not collect
579
+ // a field the plan gives the form is built from the plan, told what the
580
+ // source collects (AGL-3143 §14): copying it could only end in the review
581
+ // below. A source this step cannot read still copies.
582
+ const source = (creation == null ? void 0 : creation.duplicateOf) ? inventory.forms.find((form)=>form.id === creation.duplicateOf) : undefined;
583
+ const sourceReview = source ? await aiCopiedFormReview(firestore, {
584
+ hostId,
585
+ formId: source.id,
586
+ name: source.name,
587
+ plan
588
+ }) : null;
589
+ const sourceShort = Boolean(sourceReview == null ? void 0 : sourceReview.findings.length);
590
+ const sourceLines = source && sourceShort ? await aiFormSourceLines(firestore, {
591
+ hostId,
592
+ id: source.id,
593
+ name: source.name
594
+ }) : [];
595
+ if ((creation == null ? void 0 : creation.duplicateOf) && source && !sourceShort) {
547
596
  const copy = await duplicate('form', {
548
597
  orgId: job.orgId,
549
598
  hostId,
@@ -625,10 +674,13 @@ export function createAiJobFormStep(deps = {}) {
625
674
  messages: [
626
675
  {
627
676
  role: 'user',
628
- content: aiJobFormPrompt(job, plan, name, {
629
- audience,
630
- submissions: decisions.submissions
631
- })
677
+ content: [
678
+ aiJobFormPrompt(job, plan, name, {
679
+ audience,
680
+ submissions: decisions.submissions
681
+ }),
682
+ ...sourceLines
683
+ ].join('\n')
632
684
  }
633
685
  ],
634
686
  tool: AI_JOB_FORM_TOOL,