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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +2 -2
  2. package/package.json +17 -17
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +10 -2
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/components/ai-assist-provider.component.js +2 -2
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  12. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  14. package/src/lib/components/ai-job-plan.component.js +21 -4
  15. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  16. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  17. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  18. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  19. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  20. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  21. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  22. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  23. package/src/lib/components/ai-product-zones.d.ts +124 -0
  24. package/src/lib/components/ai-product-zones.js +32 -0
  25. package/src/lib/components/ai-product-zones.js.map +1 -0
  26. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  27. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  28. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  29. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  30. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  31. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  32. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  35. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.js +2 -2
  37. package/src/lib/components/assist-panel.component.js.map +1 -1
  38. package/src/lib/components/assist-signals-page.component.js +2 -2
  39. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  40. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  41. package/src/lib/jobs/ai-email-bindings.js +75 -42
  42. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  43. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  44. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  45. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-component-step.js +9 -3
  47. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  49. package/src/lib/jobs/ai-job-drafts.js +12 -5
  50. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  51. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  52. package/src/lib/jobs/ai-job-email-step.js +1 -3
  53. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  56. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  58. package/src/lib/jobs/ai-job-generation.js +10 -2
  59. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  61. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  63. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  64. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  65. package/src/lib/jobs/ai-job-page-step.js +7 -2
  66. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  68. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  69. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  70. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  71. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  73. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  74. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  75. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  76. package/src/lib/jobs/ai-job-products-step.js +54 -32
  77. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  78. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  79. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  80. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  81. package/src/lib/jobs/ai-job-template-step.js +1 -0
  82. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  85. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  87. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  88. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  89. package/src/lib/jobs/ai-workflow-records.js +60 -27
  90. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  91. package/src/lib/model/ai-allotments.d.ts +11 -3
  92. package/src/lib/model/ai-allotments.js +21 -4
  93. package/src/lib/model/ai-allotments.js.map +1 -1
  94. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  95. package/src/lib/model/ai-automation-outline.js.map +1 -1
  96. package/src/lib/model/ai-build-plan.d.ts +57 -0
  97. package/src/lib/model/ai-build-plan.js +116 -7
  98. package/src/lib/model/ai-build-plan.js.map +1 -1
  99. package/src/lib/model/ai-seo.d.ts +11 -51
  100. package/src/lib/model/ai-seo.js +0 -17
  101. package/src/lib/model/ai-seo.js.map +1 -1
  102. package/src/lib/model/ai-site-inventory.js +1 -1
  103. package/src/lib/model/ai-site-inventory.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.js +2 -2
  105. package/src/lib/model/ai-template-subjects.js.map +1 -1
  106. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  107. package/src/lib/model/ai-theme-proposal.js +1 -1
  108. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  109. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  110. package/src/lib/model/ai-workflow-job.js +1 -2
  111. package/src/lib/model/ai-workflow-job.js.map +1 -1
  112. package/src/lib/plan-entitlements.d.ts +149 -0
  113. package/src/lib/plan-entitlements.js +168 -0
  114. package/src/lib/plan-entitlements.js.map +1 -0
  115. package/src/lib/plugin.js +1 -1
  116. package/src/lib/plugin.js.map +1 -1
  117. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  118. package/src/lib/runtime/ai-doctrine-validators.js +179 -23
  119. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  120. package/src/lib/runtime/ai-doctrine.js +5 -5
  121. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  122. package/src/lib/runtime/ai-eval-live.js +29 -14
  123. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  124. package/src/lib/runtime/ai-eval.d.ts +8 -1
  125. package/src/lib/runtime/ai-eval.js +29 -2
  126. package/src/lib/runtime/ai-eval.js.map +1 -1
  127. package/src/lib/runtime/ai-node-tree.js +3 -3
  128. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  129. package/src/lib/runtime/ai-palette.d.ts +6 -0
  130. package/src/lib/runtime/ai-palette.generated.js +21 -255
  131. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  132. package/src/lib/runtime/ai-palette.js +7 -1
  133. package/src/lib/runtime/ai-palette.js.map +1 -1
  134. package/src/lib/runtime/seo-audit.d.ts +13 -63
  135. package/src/lib/runtime/seo-audit.js +22 -207
  136. package/src/lib/runtime/seo-audit.js.map +1 -1
  137. package/src/lib/runtime/site-inventory.js +1 -1
  138. package/src/lib/runtime/site-inventory.js.map +1 -1
  139. package/src/lib/server/ai-admin-org.js +4 -25
  140. package/src/lib/server/ai-admin-org.js.map +1 -1
  141. package/src/lib/server/ai-admin-user.js +2 -19
  142. package/src/lib/server/ai-admin-user.js.map +1 -1
  143. package/src/lib/server/ai-assist-prompts.js +3 -3
  144. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  145. package/src/lib/server/ai-insight-digest-route.js +8 -1
  146. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  147. package/src/lib/server/assist-chat.js +11 -10
  148. package/src/lib/server/assist-chat.js.map +1 -1
  149. package/src/lib/server/assist-view-context.js +23 -23
  150. package/src/lib/server/assist-view-context.js.map +1 -1
  151. package/src/lib/server.d.ts +1 -0
  152. package/src/lib/server.js.map +1 -1
  153. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  154. package/src/lib/testing/stand-in-automation-index.js +102 -0
  155. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  156. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  157. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  158. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  159. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  160. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  161. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  162. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  163. package/src/lib/testing/stand-in-product-index.js +105 -0
  164. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  165. package/src/lib/tools/ai-component-tool.js +2 -2
  166. package/src/lib/tools/ai-component-tool.js.map +1 -1
  167. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  168. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  169. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  170. package/src/lib/tools/ai-seo-tool.js +7 -38
  171. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  172. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  173. package/src/lib/tools/ai-workflow-tool.js +1 -1
  174. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  175. package/src/lib/usage/ai-allotment-alerts.d.ts +3 -0
  176. package/src/lib/usage/ai-allotment-alerts.js +20 -8
  177. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  178. package/src/lib/usage/assist-free-taste.js +2 -1
  179. package/src/lib/usage/assist-free-taste.js.map +1 -1
  180. package/src/lib/usage/assist-signal-mining.js +1 -1
  181. package/src/lib/usage/assist-signal-mining.js.map +1 -1
  182. package/src/lib/usage-axes.d.ts +39 -0
  183. package/src/lib/usage-axes.js +70 -0
  184. package/src/lib/usage-axes.js.map +1 -0
  185. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  186. package/src/lib/runtime/seo-page-facts.js +0 -150
  187. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n type AssistRefusedBy,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { ORG_PERMISSIONS } from '@aglyn/aglyn/app-utils/org-permissions'\n\n/**\n * AI ALLOTMENTS (AGL-2942): the pure half.\n *\n * The workspace's AI pool is one band. An allotment is a manager's decision\n * about how much of that band one SUBJECT may draw in a calendar month (UTC),\n * so one client site, one contractor or one teammate cannot spend what the\n * others were counting on:\n *\n * orgs/{orgId}/aiAllotments/{subject}\n * { subject, scope, uid, hostId, credits, mode, models, setBy,\n * updatedAt, alerted }\n *\n * where `subject` is one of\n *\n * member:{uid} a team member, across every site\n * collab:{hostId}:{uid} a site collaborator, on that site\n * host:{hostId} a whole site, across everyone who works on it\n * org the org-wide model restriction (no credits)\n *\n * ## An allotment never grants\n *\n * The org band stays the outer wall: the reservation decides the workspace's\n * own ceilings first and consults an allotment only once they admitted the\n * request. So an allotment larger than the band is not an error and not a\n * grant — it is a line the workspace reaches after the band.\n *\n * ## Hard and soft\n *\n * A HARD allotment refuses the request that finds the subject at or past it.\n * A SOFT one admits it and tells the subject and the workspace's billing\n * contacts at 80% and again at 100%. Both are measured against what the\n * subject already drew this month, before the request — the same shape the\n * band is measured in, so a request that starts under the line may finish\n * past it, and the next one is the one refused.\n *\n * Everything here is dependency-light so the gate, the routes and the console\n * cards read one definition of a subject, a standing and a refusal sentence.\n */\n\n/** `orgs/{orgId}/{this}/{subject}`. */\nexport const AI_ALLOTMENTS_COLLECTION = 'aiAllotments'\n\n/** The subject id of the org-wide model restriction. */\nexport const AI_ALLOTMENT_ORG_SUBJECT = 'org'\n\nexport type AiAllotmentMode = 'hard' | 'soft'\n\nexport type AiAllotmentScope = 'member' | 'collab' | 'host' | 'org'\n\n/** The scopes an allotment of CREDITS can have; `org` carries models only. */\nexport type AiCreditAllotmentScope = Exclude<AiAllotmentScope, 'org'>\n\n/**\n * Why a reservation refused: the meter's own ceilings, and an allotment.\n *\n * Widened here rather than in core's `AssistRefusedBy`, because an allotment\n * is the plugin's rung: core names the ceilings core's billing sells.\n */\nexport type AiRefusedBy = AssistRefusedBy | 'allotment'\n\n/** The two thresholds a soft allotment announces. */\nexport type AiAllotmentThreshold = 80 | 100\n\n/** The share of an allotment at which the strip warns and a soft one alerts. */\nexport const AI_ALLOTMENT_WARN_SHARE = 0.8\n\n/**\n * The largest monthly allotment a write may carry. A bound on a typing\n * mistake, not a product limit: a billion credits is a million dollars of\n * provider spend, far past any band the platform sells.\n */\nexport const AI_ALLOTMENT_MAX_CREDITS = 1_000_000_000\n\n/** The most catalog ids one allowlist may name. */\nexport const AI_ALLOTMENT_MAX_MODELS = 20\n\nconst ID_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport interface AiAllotmentSubject {\n id: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n}\n\nexport type AiAllotmentSubjectInput =\n | { scope: 'member'; uid: string }\n | { scope: 'collab'; hostId: string; uid: string }\n | { scope: 'host'; hostId: string }\n | { scope: 'org' }\n\n/** The document id for a subject. Throws on a segment that is not an id. */\nexport function aiAllotmentSubjectId(input: AiAllotmentSubjectInput): string {\n const segment = (value: string): string => {\n if (!ID_SEGMENT.test(value)) throw new Error(`not an id segment: ${value}`)\n return value\n }\n switch (input.scope) {\n case 'member':\n return `member:${segment(input.uid)}`\n case 'collab':\n return `collab:${segment(input.hostId)}:${segment(input.uid)}`\n case 'host':\n return `host:${segment(input.hostId)}`\n default:\n return AI_ALLOTMENT_ORG_SUBJECT\n }\n}\n\n/** A subject id read back, or `null` for anything that is not one. */\nexport function parseAiAllotmentSubject(value: unknown): AiAllotmentSubject | null {\n if (typeof value !== 'string') return null\n if (value === AI_ALLOTMENT_ORG_SUBJECT) {\n return { id: value, scope: 'org', uid: null, hostId: null }\n }\n const parts = value.split(':')\n if (parts.some((part) => !ID_SEGMENT.test(part))) return null\n if (parts.length === 2 && parts[0] === 'member') {\n return { id: value, scope: 'member', uid: parts[1], hostId: null }\n }\n if (parts.length === 2 && parts[0] === 'host') {\n return { id: value, scope: 'host', uid: null, hostId: parts[1] }\n }\n if (parts.length === 3 && parts[0] === 'collab') {\n return { id: value, scope: 'collab', uid: parts[2], hostId: parts[1] }\n }\n return null\n}\n\n/** One allotment as every reader hands it on. */\nexport interface AiAllotment {\n subject: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n /**\n * Credits a month, or `null` for an allotment that bounds models alone —\n * and always `null` on the org-wide restriction.\n */\n credits: number | null\n mode: AiAllotmentMode\n /** Catalog ids the subject may run on, or `null` for no restriction. */\n models: string[] | null\n setBy: string | null\n /** The last threshold announced, and the month it was announced in. */\n alerted: { month: string; threshold: AiAllotmentThreshold } | null\n}\n\n/** A finite positive whole number of credits, else `null`. */\nexport function aiAllotmentCredits(value: unknown): number | null {\n const parsed = typeof value === 'number' ? value : Number.NaN\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.min(AI_ALLOTMENT_MAX_CREDITS, Math.floor(parsed))\n}\n\n/** An allowlist read back: unique strings, bounded; empty reads as none. */\nexport function aiAllotmentModels(value: unknown): string[] | null {\n if (!Array.isArray(value)) return null\n const models = [\n ...new Set(\n value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter(Boolean),\n ),\n ].slice(0, AI_ALLOTMENT_MAX_MODELS)\n return models.length ? models : null\n}\n\n/**\n * A stored allotment as a complete record, or `null` when the id is not a\n * subject. The scope, uid and site are read off the ID, never off the\n * fields: the id is what the gate addresses, so it is the only claim about\n * who the document is for that can be trusted.\n *\n * `mode` defaults to `hard`: a document whose mode was lost is a cap, which\n * is the reading that cannot spend more than a manager set.\n */\nexport function aiAllotmentFrom(\n raw: Record<string, unknown> | null | undefined,\n id: string,\n): AiAllotment | null {\n const subject = parseAiAllotmentSubject(id)\n if (!subject || !raw) return null\n const alertedRaw = raw['alerted'] as { month?: unknown; threshold?: unknown } | null\n const threshold = Number(alertedRaw?.threshold)\n return {\n subject: subject.id,\n scope: subject.scope,\n uid: subject.uid,\n hostId: subject.hostId,\n credits: subject.scope === 'org' ? null : aiAllotmentCredits(raw['credits']),\n mode: raw['mode'] === 'soft' ? 'soft' : 'hard',\n models: aiAllotmentModels(raw['models']),\n setBy: typeof raw['setBy'] === 'string' ? (raw['setBy'] as string) : null,\n alerted:\n typeof alertedRaw?.month === 'string' && (threshold === 80 || threshold === 100)\n ? { month: alertedRaw.month, threshold }\n : null,\n }\n}\n\n/** One allotment of credits measured against what its subject drew. */\nexport interface AiAllotmentStanding {\n subject: string\n scope: AiCreditAllotmentScope\n uid: string | null\n hostId: string | null\n credits: number\n /** Credits drawn this month before the request being decided. */\n used: number\n mode: AiAllotmentMode\n}\n\nexport type AiAllotmentState = 'ok' | 'warn' | 'reached'\n\n/** `reached` at or past the allotment, `warn` from 80% of it. */\nexport function aiAllotmentState(used: number, credits: number): AiAllotmentState {\n if (!(credits > 0)) return 'ok'\n if (used >= credits) return 'reached'\n if (used >= credits * AI_ALLOTMENT_WARN_SHARE) return 'warn'\n return 'ok'\n}\n\n/** The threshold a standing sits at, for the alert pipeline. */\nexport function aiAllotmentThreshold(\n used: number,\n credits: number,\n): AiAllotmentThreshold | null {\n const state = aiAllotmentState(used, credits)\n return state === 'reached' ? 100 : state === 'warn' ? 80 : null\n}\n\n/**\n * Whether a standing admits a request.\n *\n * A soft allotment always does. A hard one admits while the subject is under\n * it — and, for a caller that knows what the request will cost before it\n * starts (a job's estimate), while the estimate still fits: a 100-credit job\n * at 0 of 100 is admitted, one at 1 of 100 is not.\n */\nexport function aiAllotmentAdmits(\n standing: Pick<AiAllotmentStanding, 'credits' | 'used' | 'mode'>,\n estimateCredits = 0,\n): boolean {\n if (standing.mode === 'soft') return true\n const estimate = Number.isFinite(estimateCredits) && estimateCredits > 0 ? estimateCredits : 0\n return estimate > 0\n ? standing.used + estimate <= standing.credits\n : standing.used < standing.credits\n}\n\n/**\n * Which subject a refusal names when more than one allotment is spent: the\n * narrowest first — the person on this site, then the person, then the site.\n */\nconst SCOPE_ORDER: readonly AiCreditAllotmentScope[] = ['collab', 'member', 'host']\n\nconst scopeRank = (scope: AiCreditAllotmentScope): number => SCOPE_ORDER.indexOf(scope)\n\nexport interface AiAllotmentVerdict {\n /** The hard allotment that refuses the request, or `null`. */\n refusal: AiAllotmentStanding | null\n /** The allotment with the least left, for the usage strip; `null` for none. */\n binding: AiAllotmentStanding | null\n /** Soft allotments at a threshold, for the alert pipeline. */\n alerts: Array<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The verdict over every allotment that applies to one request. */\nexport function aiAllotmentVerdict(\n standings: readonly AiAllotmentStanding[],\n estimateCredits = 0,\n): AiAllotmentVerdict {\n const ordered = [...standings].sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope))\n const refusal =\n ordered.find((standing) => !aiAllotmentAdmits(standing, estimateCredits)) ?? null\n let binding: AiAllotmentStanding | null = null\n for (const standing of ordered) {\n if (!binding || standing.credits - standing.used < binding.credits - binding.used) {\n binding = standing\n }\n }\n const alerts = ordered.flatMap((standing) => {\n if (standing.mode !== 'soft') return []\n const threshold = aiAllotmentThreshold(standing.used, standing.credits)\n return threshold ? [{ standing, threshold }] : []\n })\n return { refusal, binding, alerts }\n}\n\n/**\n * The allowlists that apply to a request, intersected — `null` when none of\n * the allotments names one. An empty result is a real answer: two lists that\n * share no model leave nothing to pick, and the model choice falls back to\n * the routing table rather than turning AI off for a mistake in a list.\n */\nexport function aiAllotmentModelsAllowed(\n allotments: ReadonlyArray<Pick<AiAllotment, 'models'> | null | undefined>,\n): string[] | null {\n const lists = allotments\n .map((allotment) => allotment?.models)\n .filter((models): models is string[] => Array.isArray(models) && models.length > 0)\n if (!lists.length) return null\n return lists.reduce((kept, list) => kept.filter((id) => list.includes(id)))\n}\n\n/** The catalog label for `billing.manage`, the word a sentence names it by. */\nexport function billingManageLabel(): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === 'billing.manage')?.label ??\n 'Manage billing'\n )\n}\n\n/**\n * The sentence a refused request is told, naming who can raise the\n * allotment and where. No figures: the strip beside it carries those, and a\n * refusal that quoted a count would be stale the moment someone raised it.\n */\nexport function aiAllotmentRefusalText(\n scope: AiCreditAllotmentScope | null | undefined,\n): string {\n const managers = `an organization admin with the \"${billingManageLabel()}\" permission`\n switch (scope) {\n case 'collab':\n return (\n 'You have used your AI allotment on this site for this month. ' +\n `The site's admin, or ${managers}, can raise it on the site's Users card.`\n )\n case 'host':\n return (\n 'This site has used its AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n default:\n return (\n 'You have used your AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n }\n}\n\n/**\n * The words a soft allotment's alert carries, for BOTH channels — the\n * console notification and the email — so the two cannot say different\n * things about one crossing. `name` is the member's name or the site's.\n */\nexport function aiAllotmentAlertCopy(input: {\n scope: AiCreditAllotmentScope\n threshold: AiAllotmentThreshold\n used: number\n credits: number\n name: string\n}): { title: string; body: string } {\n const who =\n input.scope === 'host' ? `The ${input.name} site` : input.name\n const whose = input.scope === 'host' ? 'its' : 'their'\n const where = input.scope === 'collab' ? ' on its site' : ''\n const figures =\n `${input.used.toLocaleString('en-US')} of ` +\n `${input.credits.toLocaleString('en-US')} credits used this month.`\n if (input.threshold >= 100) {\n return {\n title: `${who} has used ${whose} whole AI allotment${where}`,\n body:\n `${figures} The allotment is soft, so requests keep working and ` +\n 'draw on the workspace’s pool. Raise it or make it hard under ' +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION}.`,\n }\n }\n return {\n title: `${who} is past ${Math.round(AI_ALLOTMENT_WARN_SHARE * 100)}% of ${whose} AI allotment${where}`,\n body: `${figures} The allotment is soft, so nothing stops at the line.`,\n }\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LOCATION","ORG_PERMISSIONS","AI_ALLOTMENTS_COLLECTION","AI_ALLOTMENT_ORG_SUBJECT","AI_ALLOTMENT_WARN_SHARE","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","ID_SEGMENT","aiAllotmentSubjectId","input","segment","value","test","Error","scope","uid","hostId","parseAiAllotmentSubject","id","parts","split","some","part","length","aiAllotmentCredits","parsed","Number","NaN","isFinite","Math","min","floor","aiAllotmentModels","Array","isArray","models","Set","filter","entry","map","trim","Boolean","slice","aiAllotmentFrom","raw","subject","alertedRaw","threshold","credits","mode","setBy","alerted","month","aiAllotmentState","used","aiAllotmentThreshold","state","aiAllotmentAdmits","standing","estimateCredits","estimate","SCOPE_ORDER","scopeRank","indexOf","aiAllotmentVerdict","standings","ordered","sort","a","b","refusal","find","binding","alerts","flatMap","aiAllotmentModelsAllowed","allotments","lists","allotment","reduce","kept","list","includes","billingManageLabel","definition","key","label","aiAllotmentRefusalText","managers","aiAllotmentAlertCopy","who","name","whose","where","figures","toLocaleString","title","body","round"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n type AssistRefusedBy,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { ORG_PERMISSIONS } from '@aglyn/aglyn/app-utils/org-permissions'\n\n/**\n * AI ALLOTMENTS (AGL-2942): the pure half.\n *\n * The workspace's AI pool is one band. An allotment is a manager's decision\n * about how much of that band one SUBJECT may draw in a calendar month (UTC),\n * so one client site, one contractor or one teammate cannot spend what the\n * others were counting on:\n *\n * orgs/{orgId}/aiAllotments/{subject}\n * { subject, scope, uid, hostId, credits, mode, models, setBy,\n * updatedAt, alerted }\n *\n * where `subject` is one of\n *\n * member:{uid} a team member, across every site\n * collab:{hostId}:{uid} a site collaborator, on that site\n * host:{hostId} a whole site, across everyone who works on it\n * org the org-wide model restriction (no credits)\n *\n * ## An allotment never grants\n *\n * The org band stays the outer wall: the reservation decides the workspace's\n * own ceilings first and consults an allotment only once they admitted the\n * request. So an allotment larger than the band is not an error and not a\n * grant — it is a line the workspace reaches after the band.\n *\n * ## Hard and soft\n *\n * A HARD allotment refuses the request that finds the subject at or past it.\n * A SOFT one admits it and tells the subject and the workspace's billing\n * contacts at 80% and again at 100%. Both are measured against what the\n * subject already drew this month, before the request — the same shape the\n * band is measured in, so a request that starts under the line may finish\n * past it, and the next one is the one refused.\n *\n * Everything here is dependency-light so the gate, the routes and the console\n * cards read one definition of a subject, a standing and a refusal sentence.\n */\n\n/** `orgs/{orgId}/{this}/{subject}`. */\nexport const AI_ALLOTMENTS_COLLECTION = 'aiAllotments'\n\n/** The subject id of the org-wide model restriction. */\nexport const AI_ALLOTMENT_ORG_SUBJECT = 'org'\n\nexport type AiAllotmentMode = 'hard' | 'soft'\n\nexport type AiAllotmentScope = 'member' | 'collab' | 'host' | 'org'\n\n/** The scopes an allotment of CREDITS can have; `org` carries models only. */\nexport type AiCreditAllotmentScope = Exclude<AiAllotmentScope, 'org'>\n\n/**\n * Why a reservation refused: the meter's own ceilings, and an allotment.\n *\n * Widened here rather than in core's `AssistRefusedBy`, because an allotment\n * is the plugin's rung: core names the ceilings core's billing sells.\n */\nexport type AiRefusedBy = AssistRefusedBy | 'allotment'\n\n/**\n * The steps a soft allotment announces — the same 75, 80, 90 and 100 every\n * usage quota steps through (AGL-3431). Only the highest step reached is\n * sent, each once a month.\n */\nexport const AI_ALLOTMENT_ALERT_BANDS = [75, 80, 90, 100] as const\nexport type AiAllotmentThreshold = (typeof AI_ALLOTMENT_ALERT_BANDS)[number]\n\n/** The share of an allotment at which the strip warns and a soft one alerts. */\nexport const AI_ALLOTMENT_WARN_SHARE = 0.8\n\n/**\n * The largest monthly allotment a write may carry. A bound on a typing\n * mistake, not a product limit: a billion credits is a million dollars of\n * provider spend, far past any band the platform sells.\n */\nexport const AI_ALLOTMENT_MAX_CREDITS = 1_000_000_000\n\n/** The most catalog ids one allowlist may name. */\nexport const AI_ALLOTMENT_MAX_MODELS = 20\n\nconst ID_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport interface AiAllotmentSubject {\n id: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n}\n\nexport type AiAllotmentSubjectInput =\n | { scope: 'member'; uid: string }\n | { scope: 'collab'; hostId: string; uid: string }\n | { scope: 'host'; hostId: string }\n | { scope: 'org' }\n\n/** The document id for a subject. Throws on a segment that is not an id. */\nexport function aiAllotmentSubjectId(input: AiAllotmentSubjectInput): string {\n const segment = (value: string): string => {\n if (!ID_SEGMENT.test(value)) throw new Error(`not an id segment: ${value}`)\n return value\n }\n switch (input.scope) {\n case 'member':\n return `member:${segment(input.uid)}`\n case 'collab':\n return `collab:${segment(input.hostId)}:${segment(input.uid)}`\n case 'host':\n return `host:${segment(input.hostId)}`\n default:\n return AI_ALLOTMENT_ORG_SUBJECT\n }\n}\n\n/** A subject id read back, or `null` for anything that is not one. */\nexport function parseAiAllotmentSubject(value: unknown): AiAllotmentSubject | null {\n if (typeof value !== 'string') return null\n if (value === AI_ALLOTMENT_ORG_SUBJECT) {\n return { id: value, scope: 'org', uid: null, hostId: null }\n }\n const parts = value.split(':')\n if (parts.some((part) => !ID_SEGMENT.test(part))) return null\n if (parts.length === 2 && parts[0] === 'member') {\n return { id: value, scope: 'member', uid: parts[1], hostId: null }\n }\n if (parts.length === 2 && parts[0] === 'host') {\n return { id: value, scope: 'host', uid: null, hostId: parts[1] }\n }\n if (parts.length === 3 && parts[0] === 'collab') {\n return { id: value, scope: 'collab', uid: parts[2], hostId: parts[1] }\n }\n return null\n}\n\n/** One allotment as every reader hands it on. */\nexport interface AiAllotment {\n subject: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n /**\n * Credits a month, or `null` for an allotment that bounds models alone —\n * and always `null` on the org-wide restriction.\n */\n credits: number | null\n mode: AiAllotmentMode\n /** Catalog ids the subject may run on, or `null` for no restriction. */\n models: string[] | null\n setBy: string | null\n /** The last threshold announced, and the month it was announced in. */\n alerted: { month: string; threshold: AiAllotmentThreshold } | null\n}\n\n/** A finite positive whole number of credits, else `null`. */\nexport function aiAllotmentCredits(value: unknown): number | null {\n const parsed = typeof value === 'number' ? value : Number.NaN\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.min(AI_ALLOTMENT_MAX_CREDITS, Math.floor(parsed))\n}\n\n/** An allowlist read back: unique strings, bounded; empty reads as none. */\nexport function aiAllotmentModels(value: unknown): string[] | null {\n if (!Array.isArray(value)) return null\n const models = [\n ...new Set(\n value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter(Boolean),\n ),\n ].slice(0, AI_ALLOTMENT_MAX_MODELS)\n return models.length ? models : null\n}\n\n/**\n * A stored allotment as a complete record, or `null` when the id is not a\n * subject. The scope, uid and site are read off the ID, never off the\n * fields: the id is what the gate addresses, so it is the only claim about\n * who the document is for that can be trusted.\n *\n * `mode` defaults to `hard`: a document whose mode was lost is a cap, which\n * is the reading that cannot spend more than a manager set.\n */\nexport function aiAllotmentFrom(\n raw: Record<string, unknown> | null | undefined,\n id: string,\n): AiAllotment | null {\n const subject = parseAiAllotmentSubject(id)\n if (!subject || !raw) return null\n const alertedRaw = raw['alerted'] as { month?: unknown; threshold?: unknown } | null\n const threshold = Number(alertedRaw?.threshold)\n return {\n subject: subject.id,\n scope: subject.scope,\n uid: subject.uid,\n hostId: subject.hostId,\n credits: subject.scope === 'org' ? null : aiAllotmentCredits(raw['credits']),\n mode: raw['mode'] === 'soft' ? 'soft' : 'hard',\n models: aiAllotmentModels(raw['models']),\n setBy: typeof raw['setBy'] === 'string' ? (raw['setBy'] as string) : null,\n alerted:\n typeof alertedRaw?.month === 'string' && (threshold === 80 || threshold === 100)\n ? { month: alertedRaw.month, threshold }\n : null,\n }\n}\n\n/** One allotment of credits measured against what its subject drew. */\nexport interface AiAllotmentStanding {\n subject: string\n scope: AiCreditAllotmentScope\n uid: string | null\n hostId: string | null\n credits: number\n /** Credits drawn this month before the request being decided. */\n used: number\n mode: AiAllotmentMode\n}\n\nexport type AiAllotmentState = 'ok' | 'warn' | 'reached'\n\n/** `reached` at or past the allotment, `warn` from 80% of it. */\nexport function aiAllotmentState(used: number, credits: number): AiAllotmentState {\n if (!(credits > 0)) return 'ok'\n if (used >= credits) return 'reached'\n if (used >= credits * AI_ALLOTMENT_WARN_SHARE) return 'warn'\n return 'ok'\n}\n\n/**\n * The highest step a standing has reached, for the alert pipeline — compared\n * as `used × 100 ≥ step × credits`, so 9 of 10 is exactly 90%.\n */\nexport function aiAllotmentThreshold(\n used: number,\n credits: number,\n): AiAllotmentThreshold | null {\n if (!(credits > 0) || !Number.isFinite(used) || used < 0) return null\n for (let index = AI_ALLOTMENT_ALERT_BANDS.length - 1; index >= 0; index -= 1) {\n const band = AI_ALLOTMENT_ALERT_BANDS[index]\n if (used * 100 >= band * credits) return band\n }\n return null\n}\n\n/**\n * Whether a standing admits a request.\n *\n * A soft allotment always does. A hard one admits while the subject is under\n * it — and, for a caller that knows what the request will cost before it\n * starts (a job's estimate), while the estimate still fits: a 100-credit job\n * at 0 of 100 is admitted, one at 1 of 100 is not.\n */\nexport function aiAllotmentAdmits(\n standing: Pick<AiAllotmentStanding, 'credits' | 'used' | 'mode'>,\n estimateCredits = 0,\n): boolean {\n if (standing.mode === 'soft') return true\n const estimate = Number.isFinite(estimateCredits) && estimateCredits > 0 ? estimateCredits : 0\n return estimate > 0\n ? standing.used + estimate <= standing.credits\n : standing.used < standing.credits\n}\n\n/**\n * Which subject a refusal names when more than one allotment is spent: the\n * narrowest first — the person on this site, then the person, then the site.\n */\nconst SCOPE_ORDER: readonly AiCreditAllotmentScope[] = ['collab', 'member', 'host']\n\nconst scopeRank = (scope: AiCreditAllotmentScope): number => SCOPE_ORDER.indexOf(scope)\n\nexport interface AiAllotmentVerdict {\n /** The hard allotment that refuses the request, or `null`. */\n refusal: AiAllotmentStanding | null\n /** The allotment with the least left, for the usage strip; `null` for none. */\n binding: AiAllotmentStanding | null\n /** Soft allotments at a threshold, for the alert pipeline. */\n alerts: Array<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The verdict over every allotment that applies to one request. */\nexport function aiAllotmentVerdict(\n standings: readonly AiAllotmentStanding[],\n estimateCredits = 0,\n): AiAllotmentVerdict {\n const ordered = [...standings].sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope))\n const refusal =\n ordered.find((standing) => !aiAllotmentAdmits(standing, estimateCredits)) ?? null\n let binding: AiAllotmentStanding | null = null\n for (const standing of ordered) {\n if (!binding || standing.credits - standing.used < binding.credits - binding.used) {\n binding = standing\n }\n }\n const alerts = ordered.flatMap((standing) => {\n if (standing.mode !== 'soft') return []\n const threshold = aiAllotmentThreshold(standing.used, standing.credits)\n return threshold ? [{ standing, threshold }] : []\n })\n return { refusal, binding, alerts }\n}\n\n/**\n * The allowlists that apply to a request, intersected — `null` when none of\n * the allotments names one. An empty result is a real answer: two lists that\n * share no model leave nothing to pick, and the model choice falls back to\n * the routing table rather than turning AI off for a mistake in a list.\n */\nexport function aiAllotmentModelsAllowed(\n allotments: ReadonlyArray<Pick<AiAllotment, 'models'> | null | undefined>,\n): string[] | null {\n const lists = allotments\n .map((allotment) => allotment?.models)\n .filter((models): models is string[] => Array.isArray(models) && models.length > 0)\n if (!lists.length) return null\n return lists.reduce((kept, list) => kept.filter((id) => list.includes(id)))\n}\n\n/** The catalog label for `billing.manage`, the word a sentence names it by. */\nexport function billingManageLabel(): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === 'billing.manage')?.label ??\n 'Manage billing'\n )\n}\n\n/**\n * The sentence a refused request is told, naming who can raise the\n * allotment and where. No figures: the strip beside it carries those, and a\n * refusal that quoted a count would be stale the moment someone raised it.\n */\nexport function aiAllotmentRefusalText(\n scope: AiCreditAllotmentScope | null | undefined,\n): string {\n const managers = `an organization admin with the \"${billingManageLabel()}\" permission`\n switch (scope) {\n case 'collab':\n return (\n 'You have used your AI allotment on this site for this month. ' +\n `The site's admin, or ${managers}, can raise it on the site's Users card.`\n )\n case 'host':\n return (\n 'This site has used its AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n default:\n return (\n 'You have used your AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n }\n}\n\n/**\n * The words a soft allotment's alert carries, for BOTH channels — the\n * console notification and the email — so the two cannot say different\n * things about one crossing. `name` is the member's name or the site's.\n */\nexport function aiAllotmentAlertCopy(input: {\n scope: AiCreditAllotmentScope\n threshold: AiAllotmentThreshold\n used: number\n credits: number\n name: string\n}): { title: string; body: string } {\n const who =\n input.scope === 'host' ? `The ${input.name} site` : input.name\n const whose = input.scope === 'host' ? 'its' : 'their'\n const where = input.scope === 'collab' ? ' on its site' : ''\n const figures =\n `${input.used.toLocaleString('en-US')} of ` +\n `${input.credits.toLocaleString('en-US')} credits used this month.`\n if (input.threshold >= 100) {\n return {\n title: `${who} has used ${whose} whole AI allotment${where}`,\n body:\n `${figures} The allotment is soft, so requests keep working and ` +\n 'draw on the workspace’s pool. Raise it or make it hard under ' +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION}.`,\n }\n }\n return {\n title: `${who} is past ${input.threshold}% of ${whose} AI allotment${where}`,\n body: `${figures} The allotment is soft, so nothing stops at the line.`,\n }\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LOCATION","ORG_PERMISSIONS","AI_ALLOTMENTS_COLLECTION","AI_ALLOTMENT_ORG_SUBJECT","AI_ALLOTMENT_ALERT_BANDS","AI_ALLOTMENT_WARN_SHARE","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","ID_SEGMENT","aiAllotmentSubjectId","input","segment","value","test","Error","scope","uid","hostId","parseAiAllotmentSubject","id","parts","split","some","part","length","aiAllotmentCredits","parsed","Number","NaN","isFinite","Math","min","floor","aiAllotmentModels","Array","isArray","models","Set","filter","entry","map","trim","Boolean","slice","aiAllotmentFrom","raw","subject","alertedRaw","threshold","credits","mode","setBy","alerted","month","aiAllotmentState","used","aiAllotmentThreshold","index","band","aiAllotmentAdmits","standing","estimateCredits","estimate","SCOPE_ORDER","scopeRank","indexOf","aiAllotmentVerdict","standings","ordered","sort","a","b","refusal","find","binding","alerts","flatMap","aiAllotmentModelsAllowed","allotments","lists","allotment","reduce","kept","list","includes","billingManageLabel","definition","key","label","aiAllotmentRefusalText","managers","aiAllotmentAlertCopy","who","name","whose","where","figures","toLocaleString","title","body"],"mappings":"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"}
@@ -15,7 +15,6 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { type HostAction, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
18
- import type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows';
19
18
  import type { AiAutomationNamedRecord, AiAutomationRecords } from './ai-automation-draft';
20
19
  import type { AiWorkflowExplanation } from '../tools/ai-workflow-tool';
21
20
  /** How much of an email's text an outline quotes. */
@@ -32,8 +31,28 @@ export declare const AI_OUTLINE_RECORD_KINDS: Readonly<Partial<Record<HostAction
32
31
  export declare function aiActionRecordKinds(action: Pick<HostAction, 'steps'>): Array<keyof AiAutomationRecords>;
33
32
  /** A saved action as an outline. */
34
33
  export declare function aiActionOutline(action: HostAction, records: AiAutomationRecords | null): string;
34
+ /**
35
+ * A saved workflow as the workflows plugin's `workflow` record index shares
36
+ * it: its name, and the facts that index documents — the stored step list
37
+ * (each a function call by `functionId`, `functionName`, `args` and
38
+ * `resultName`), the scope name it returns, and its trigger.
39
+ */
40
+ export interface AiIndexedWorkflow {
41
+ name: string;
42
+ steps?: Array<{
43
+ functionId?: string;
44
+ functionName?: string;
45
+ args?: string[];
46
+ resultName?: string;
47
+ }>;
48
+ returnValue?: string | null;
49
+ trigger?: {
50
+ event?: string;
51
+ filter?: string;
52
+ } | null;
53
+ }
35
54
  /** A saved workflow — a pipeline of function calls — as an outline. */
36
- export declare function aiWorkflowOutline(workflow: HostWorkflow, functions: readonly AiAutomationNamedRecord[] | null): string;
55
+ export declare function aiWorkflowOutline(workflow: AiIndexedWorkflow, functions: readonly AiAutomationNamedRecord[] | null): string;
37
56
  /** What the run history recorded about one run, as far as an outline reads it. */
38
57
  export interface AiRunRecord {
39
58
  result?: unknown;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-automation-outline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CLIENT_ACTION_STEP_TYPES,\n HOST_ACTION_STEP_LABELS,\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n normalizeTriggerConditions,\n} from '@aglyn/aglyn/app-utils/actions'\nimport { automationPlaceholderIn } from '@aglyn/aglyn/app-utils/automation-placeholders'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, type ContactLifecycleStage } from '@aglyn/aglyn/app-utils/crm'\nimport type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows'\nimport { aiAutomationTriggerLabel } from './ai-workflow-job'\nimport type { AiAutomationNamedRecord, AiAutomationRecords } from './ai-automation-draft'\nimport type { AiWorkflowExplanation } from '../tools/ai-workflow-tool'\n\n/**\n * What an explanation is asked from (AGL-2919): a saved automation, and one of\n * its failed runs, as plain lines a model reads — never the stored documents.\n *\n * WHAT IS LEFT OUT. An email address anywhere — a condition's value, an\n * email's text, a run's error naming the contact it could not find — becomes\n * `[email address]`. A teammate named by account id or address is \"a named\n * teammate\". A selector, a script or HTML an on-page step holds is not shown\n * at all; the step is named by its label. A run's event payload, which holds\n * what a visitor submitted, is never read. What is shown is how the automation\n * is set up, and what the run history recorded about the run.\n *\n * WHAT IS ADDED. Whether each list, campaign, workflow, webhook or dataset a\n * step names still exists on the site, looked up in code, because \"that list\n * was deleted\" is the answer to the most common failure and a model cannot\n * find it out.\n */\n\n/** An email address, wherever it appears in a line. */\nconst EMAIL_ADDRESS = /[^\\s@<>()[\\]\"',;:]+@[^\\s@<>()[\\]\"',;:]+\\.[A-Za-z]{2,}/g\n\n/** How much of an email's text an outline quotes. */\nexport const AI_OUTLINE_BODY_MAX_CHARS = 300\n/** How much of any other value an outline quotes. */\nexport const AI_OUTLINE_VALUE_MAX_CHARS = 120\n/** The most errors of one run an outline lists. */\nexport const AI_OUTLINE_RUN_ERRORS_MAX = 10\n\n/** A line with every email address replaced by what it is. */\nexport function aiRedactPersonal(text: string): string {\n return text.replace(EMAIL_ADDRESS, '[email address]')\n}\n\nfunction quoted(value: unknown, max = AI_OUTLINE_VALUE_MAX_CHARS): string {\n const text = aiRedactPersonal(String(value ?? '').replace(/\\s+/g, ' ').trim())\n return `\"${text.length > max ? `${text.slice(0, max)}…` : text}\"`\n}\n\nfunction conditionLine(condition: HostActionTriggerCondition): string {\n const field = String(condition.field ?? '')\n if (condition.op === 'notEmpty') return `${field} is not empty`\n const value = String(condition.value ?? '')\n const stage = CONTACT_LIFECYCLE_STAGE_LABELS[value as ContactLifecycleStage]\n const placeholder = automationPlaceholderIn(value) ? ' (a placeholder nobody has filled in)' : ''\n return `${field} ${condition.op} ${stage ? `\"${stage}\"` : quoted(value)}${placeholder}`\n}\n\n/** The kind of site record each step that names one names. */\nexport const AI_OUTLINE_RECORD_KINDS: Readonly<\n Partial<Record<HostActionStepType, 'lists' | 'campaigns' | 'workflows' | 'webhooks' | 'datasets'>>\n> = {\n enrollList: 'lists',\n assignCampaign: 'campaigns',\n runWorkflow: 'workflows',\n webhookPost: 'webhooks',\n datasetAppend: 'datasets',\n updateDataset: 'datasets',\n}\n\n/** The kinds of site record an action's steps name, each once. */\nexport function aiActionRecordKinds(action: Pick<HostAction, 'steps'>): Array<keyof AiAutomationRecords> {\n const kinds = (action.steps ?? []).map((step) => AI_OUTLINE_RECORD_KINDS[step?.type as HostActionStepType])\n return [...new Set(kinds.filter((kind): kind is NonNullable<typeof kind> => Boolean(kind)))]\n}\n\n/** Whether the site holds a record by the id or the name a step stores. */\nfunction standing(\n id: unknown,\n name: unknown,\n records: readonly AiAutomationNamedRecord[] | null,\n): string {\n if (automationPlaceholderIn(name) && !String(id ?? '').trim()) {\n return ' — a placeholder nobody has filled in'\n }\n if (!records) return ''\n const found = records.some(\n (record) =>\n (typeof id === 'string' && id && record.id === id) ||\n (typeof name === 'string' && name && record.name.trim() === name.trim()),\n )\n return found ? ' — the site has it' : ' — the site has none by that name'\n}\n\nfunction stepLine(\n step: HostActionStep,\n records: AiAutomationRecords | null,\n): string {\n const label = HOST_ACTION_STEP_LABELS[step.type as HostActionStepType] ?? step.type\n const detail = (() => {\n switch (step.type) {\n case 'sendEmail':\n return `subject ${quoted(step.subject)}, text ${quoted(step.body, AI_OUTLINE_BODY_MAX_CHARS)}, to the event's ${step.toField || 'email'} field`\n case 'notifyAdmins':\n return quoted(step.title)\n case 'enrollList':\n return `${quoted(step.listName || step.listId)}${standing(step.listId, step.listName, records?.lists ?? null)}`\n case 'assignCampaign':\n return `${quoted(step.campaignName || step.campaignId)}${standing(step.campaignId, step.campaignName, records?.campaigns ?? null)}`\n case 'runWorkflow':\n return `${quoted(step.workflowName || step.workflowId)}${standing(step.workflowId, step.workflowName, records?.workflows ?? null)}`\n case 'webhookPost':\n return `${quoted(step.webhookName || step.webhookId)}${standing(step.webhookId, step.webhookName, records?.webhooks ?? null)}`\n case 'datasetAppend':\n case 'updateDataset':\n return `${quoted(step.datasetName || step.datasetId)}${standing(step.datasetId, step.datasetName, records?.datasets ?? null)}`\n case 'siteAlert':\n return quoted(step.message)\n case 'wait':\n return durationOf(step.delayMinutes)\n case 'waitForEvent':\n return `${aiAutomationTriggerLabel(step.eventName)}, giving up after ${durationOf(step.timeoutMinutes)}`\n case 'exitFlow':\n return ''\n case 'setContactStage':\n return `\"${CONTACT_LIFECYCLE_STAGE_LABELS[step.lifecycleStage] ?? step.lifecycleStage}\"`\n case 'addContactTag':\n return quoted(step.tag)\n case 'assignContactOwner':\n return step.roundRobin ? 'by round robin' : 'a named teammate'\n case 'createCrmTask':\n return `${quoted(step.title)}, a ${step.kind}, due in ${step.dueInDays} ${step.dueInDays === 1 ? 'day' : 'days'}${\n step.assigneeUid || step.assigneeEmail ? ', for a named teammate' : ''\n }`\n case 'logCrmActivity':\n return `a ${step.kind}: ${quoted(step.body)}`\n case 'customEvent':\n return quoted(step.eventName)\n default:\n return CLIENT_ACTION_STEP_TYPES.has(step.type) ? 'runs in the visitor’s browser on the page' : ''\n }\n })()\n const guard = (step.when?.conditions ?? []).filter(Boolean)\n const only = guard.length\n ? ` Only if ${guard.map(conditionLine).join(step.when?.combinator === 'or' ? ' or ' : ' and ')}.`\n : ''\n return `${label}${detail ? `: ${detail}` : ''}.${only}`\n}\n\n/** Minutes as a person says them. */\nfunction durationOf(minutes: unknown): string {\n const value = Number(minutes)\n if (!Number.isFinite(value) || value <= 0) return 'no time'\n const units: Array<[number, string]> = [\n [60 * 24 * 7, 'week'],\n [60 * 24, 'day'],\n [60, 'hour'],\n [1, 'minute'],\n ]\n for (const [size, unit] of units) {\n if (value % size === 0) {\n const count = value / size\n return `${count} ${unit}${count === 1 ? '' : 's'}`\n }\n }\n return `${value} minutes`\n}\n\n/** A saved action as an outline. */\nexport function aiActionOutline(\n action: HostAction,\n records: AiAutomationRecords | null,\n): string {\n const lines = [\n `Automation: ${quoted(action.name)} — an action, switched ${action.enabled === false ? 'off' : 'on'}.`,\n `Starts when: ${aiAutomationTriggerLabel(String(action.trigger?.event ?? ''))}.`,\n ]\n const conditions = normalizeTriggerConditions(action.trigger)\n if (conditions.length) {\n lines.push(\n `Only when ${action.trigger?.combinator === 'or' ? 'any' : 'all'} of these hold: ${conditions\n .map(conditionLine)\n .join('; ')}.`,\n )\n }\n if (action.trigger?.filter?.trim()) {\n lines.push(`And only when this expression is true: ${quoted(action.trigger.filter)}.`)\n }\n lines.push('Steps, in order:')\n ;(action.steps ?? []).forEach((step, index) => {\n lines.push(`${index + 1}. ${stepLine(step, records)}`)\n })\n if (!(action.steps ?? []).length) lines.push('(none)')\n return lines.join('\\n')\n}\n\n/** A saved workflow — a pipeline of function calls — as an outline. */\nexport function aiWorkflowOutline(\n workflow: HostWorkflow,\n functions: readonly AiAutomationNamedRecord[] | null,\n): string {\n const lines = [`Workflow: ${quoted(workflow.name)} — a workflow, a list of function calls.`]\n if (workflow.trigger?.event) {\n const filter = workflow.trigger.filter?.trim()\n lines.push(\n `Starts when: ${aiAutomationTriggerLabel(workflow.trigger.event)}${\n filter ? `, only when this expression is true: ${quoted(filter)}` : ''\n }.`,\n )\n } else {\n lines.push('Starts when: it is called — by an automation, an inbound webhook or a computed variable.')\n }\n lines.push('Steps, in order:')\n ;(workflow.steps ?? []).forEach((step, index) => {\n const name = step.functionName || step.functionId || ''\n const exists = standing(step.functionId, step.functionName, functions)\n const args = (step.args ?? []).filter((arg) => String(arg ?? '').trim())\n lines.push(\n `${index + 1}. Runs the function ${quoted(name)}${exists}${\n args.length ? `, given ${args.map((arg) => quoted(arg)).join(', ')}` : ''\n }; its result is kept as ${quoted(step.resultName || `step${index + 1}`)}.`,\n )\n })\n if (!(workflow.steps ?? []).length) lines.push('(none)')\n if (workflow.returnValue?.trim()) lines.push(`Returns: ${quoted(workflow.returnValue)}.`)\n return lines.join('\\n')\n}\n\n/** What the run history recorded about one run, as far as an outline reads it. */\nexport interface AiRunRecord {\n result?: unknown\n trigger?: unknown\n summary?: unknown\n action?: unknown\n createdAt?: unknown\n}\n\nfunction whenOf(value: unknown): string | null {\n const toDate = (value as { toDate?: () => Date } | null)?.toDate\n const date =\n typeof toDate === 'function'\n ? toDate.call(value)\n : value instanceof Date\n ? value\n : null\n return date && Number.isFinite(date.getTime()) ? `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC` : null\n}\n\n/** The errors a run recorded: the prose after \"with errors:\", or a workflow's failure. */\nexport function aiRunErrors(run: AiRunRecord): string[] {\n const action = String(run.action ?? '')\n const listed = action.match(/with errors:\\s*(.+)$/)?.[1]\n if (listed) {\n return listed\n .split(/;\\s+/)\n .map((error) => error.trim())\n .filter(Boolean)\n .slice(0, AI_OUTLINE_RUN_ERRORS_MAX)\n }\n const failed = action.match(/failed on [^:]+:\\s*(.+)$/)?.[1] ?? (run.result === 'failed' ? String(run.summary ?? '') : '')\n return failed.trim() ? [failed.trim()] : []\n}\n\n/** A failed run as an outline: when, on what, what it did, and what went wrong. */\nexport function aiRunOutline(run: AiRunRecord): string {\n const when = whenOf(run.createdAt)\n const lines = [\n `The run that failed: on ${aiAutomationTriggerLabel(String(run.trigger ?? ''))}${when ? `, ${when}` : ''}.`,\n ]\n const summary = String(run.summary ?? '').trim()\n const errors = aiRunErrors(run)\n if (summary && !errors.includes(summary)) lines.push(`What it did: ${aiRedactPersonal(summary)}.`)\n lines.push('What went wrong:')\n lines.push(...(errors.length ? errors.map((error) => `- ${aiRedactPersonal(error)}`) : ['- (the run recorded no error)']))\n return lines.join('\\n')\n}\n\n/** An explanation as the text a person reads. */\nexport function aiWorkflowExplanationText(\n explanation: AiWorkflowExplanation,\n mode: 'explain' | 'diagnose',\n): string {\n const parts = [explanation.summary]\n if (explanation.points.length) {\n parts.push(\n [\n mode === 'diagnose' ? 'What happened:' : 'What it does:',\n ...explanation.points.map((point, index) => `${index + 1}. ${point}`),\n ].join('\\n'),\n )\n }\n if (explanation.suggestions.length) {\n parts.push(\n [\n mode === 'diagnose' ? 'How to fix it:' : 'Worth checking:',\n ...explanation.suggestions.map((suggestion) => `- ${suggestion}`),\n ].join('\\n'),\n )\n }\n return parts.join('\\n\\n')\n}\n"],"names":["CLIENT_ACTION_STEP_TYPES","HOST_ACTION_STEP_LABELS","normalizeTriggerConditions","automationPlaceholderIn","CONTACT_LIFECYCLE_STAGE_LABELS","aiAutomationTriggerLabel","EMAIL_ADDRESS","AI_OUTLINE_BODY_MAX_CHARS","AI_OUTLINE_VALUE_MAX_CHARS","AI_OUTLINE_RUN_ERRORS_MAX","aiRedactPersonal","text","replace","quoted","value","max","String","trim","length","slice","conditionLine","condition","field","op","stage","placeholder","AI_OUTLINE_RECORD_KINDS","enrollList","assignCampaign","runWorkflow","webhookPost","datasetAppend","updateDataset","aiActionRecordKinds","action","kinds","steps","map","step","type","Set","filter","kind","Boolean","standing","id","name","records","found","some","record","stepLine","label","detail","subject","body","toField","title","listName","listId","lists","campaignName","campaignId","campaigns","workflowName","workflowId","workflows","webhookName","webhookId","webhooks","datasetName","datasetId","datasets","message","durationOf","delayMinutes","eventName","timeoutMinutes","lifecycleStage","tag","roundRobin","dueInDays","assigneeUid","assigneeEmail","has","guard","when","conditions","only","join","combinator","minutes","Number","isFinite","units","size","unit","count","aiActionOutline","lines","enabled","trigger","event","push","forEach","index","aiWorkflowOutline","workflow","functions","functionName","functionId","exists","args","arg","resultName","returnValue","whenOf","toDate","date","call","Date","getTime","toISOString","aiRunErrors","run","listed","match","split","error","failed","result","summary","aiRunOutline","createdAt","errors","includes","aiWorkflowExplanationText","explanation","mode","parts","points","point","suggestions","suggestion"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-automation-outline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CLIENT_ACTION_STEP_TYPES,\n HOST_ACTION_STEP_LABELS,\n type HostAction,\n type HostActionStep,\n type HostActionStepType,\n type HostActionTriggerCondition,\n normalizeTriggerConditions,\n} from '@aglyn/aglyn/app-utils/actions'\nimport { automationPlaceholderIn } from '@aglyn/aglyn/app-utils/automation-placeholders'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, type ContactLifecycleStage } from '@aglyn/aglyn/app-utils/crm'\nimport { aiAutomationTriggerLabel } from './ai-workflow-job'\nimport type { AiAutomationNamedRecord, AiAutomationRecords } from './ai-automation-draft'\nimport type { AiWorkflowExplanation } from '../tools/ai-workflow-tool'\n\n/**\n * What an explanation is asked from (AGL-2919): a saved automation, and one of\n * its failed runs, as plain lines a model reads — never the stored documents.\n *\n * WHAT IS LEFT OUT. An email address anywhere — a condition's value, an\n * email's text, a run's error naming the contact it could not find — becomes\n * `[email address]`. A teammate named by account id or address is \"a named\n * teammate\". A selector, a script or HTML an on-page step holds is not shown\n * at all; the step is named by its label. A run's event payload, which holds\n * what a visitor submitted, is never read. What is shown is how the automation\n * is set up, and what the run history recorded about the run.\n *\n * WHAT IS ADDED. Whether each list, campaign, workflow, webhook or dataset a\n * step names still exists on the site, looked up in code, because \"that list\n * was deleted\" is the answer to the most common failure and a model cannot\n * find it out.\n */\n\n/** An email address, wherever it appears in a line. */\nconst EMAIL_ADDRESS = /[^\\s@<>()[\\]\"',;:]+@[^\\s@<>()[\\]\"',;:]+\\.[A-Za-z]{2,}/g\n\n/** How much of an email's text an outline quotes. */\nexport const AI_OUTLINE_BODY_MAX_CHARS = 300\n/** How much of any other value an outline quotes. */\nexport const AI_OUTLINE_VALUE_MAX_CHARS = 120\n/** The most errors of one run an outline lists. */\nexport const AI_OUTLINE_RUN_ERRORS_MAX = 10\n\n/** A line with every email address replaced by what it is. */\nexport function aiRedactPersonal(text: string): string {\n return text.replace(EMAIL_ADDRESS, '[email address]')\n}\n\nfunction quoted(value: unknown, max = AI_OUTLINE_VALUE_MAX_CHARS): string {\n const text = aiRedactPersonal(String(value ?? '').replace(/\\s+/g, ' ').trim())\n return `\"${text.length > max ? `${text.slice(0, max)}…` : text}\"`\n}\n\nfunction conditionLine(condition: HostActionTriggerCondition): string {\n const field = String(condition.field ?? '')\n if (condition.op === 'notEmpty') return `${field} is not empty`\n const value = String(condition.value ?? '')\n const stage = CONTACT_LIFECYCLE_STAGE_LABELS[value as ContactLifecycleStage]\n const placeholder = automationPlaceholderIn(value) ? ' (a placeholder nobody has filled in)' : ''\n return `${field} ${condition.op} ${stage ? `\"${stage}\"` : quoted(value)}${placeholder}`\n}\n\n/** The kind of site record each step that names one names. */\nexport const AI_OUTLINE_RECORD_KINDS: Readonly<\n Partial<Record<HostActionStepType, 'lists' | 'campaigns' | 'workflows' | 'webhooks' | 'datasets'>>\n> = {\n enrollList: 'lists',\n assignCampaign: 'campaigns',\n runWorkflow: 'workflows',\n webhookPost: 'webhooks',\n datasetAppend: 'datasets',\n updateDataset: 'datasets',\n}\n\n/** The kinds of site record an action's steps name, each once. */\nexport function aiActionRecordKinds(action: Pick<HostAction, 'steps'>): Array<keyof AiAutomationRecords> {\n const kinds = (action.steps ?? []).map((step) => AI_OUTLINE_RECORD_KINDS[step?.type as HostActionStepType])\n return [...new Set(kinds.filter((kind): kind is NonNullable<typeof kind> => Boolean(kind)))]\n}\n\n/** Whether the site holds a record by the id or the name a step stores. */\nfunction standing(\n id: unknown,\n name: unknown,\n records: readonly AiAutomationNamedRecord[] | null,\n): string {\n if (automationPlaceholderIn(name) && !String(id ?? '').trim()) {\n return ' — a placeholder nobody has filled in'\n }\n if (!records) return ''\n const found = records.some(\n (record) =>\n (typeof id === 'string' && id && record.id === id) ||\n (typeof name === 'string' && name && record.name.trim() === name.trim()),\n )\n return found ? ' — the site has it' : ' — the site has none by that name'\n}\n\nfunction stepLine(\n step: HostActionStep,\n records: AiAutomationRecords | null,\n): string {\n const label = HOST_ACTION_STEP_LABELS[step.type as HostActionStepType] ?? step.type\n const detail = (() => {\n switch (step.type) {\n case 'sendEmail':\n return `subject ${quoted(step.subject)}, text ${quoted(step.body, AI_OUTLINE_BODY_MAX_CHARS)}, to the event's ${step.toField || 'email'} field`\n case 'notifyAdmins':\n return quoted(step.title)\n case 'enrollList':\n return `${quoted(step.listName || step.listId)}${standing(step.listId, step.listName, records?.lists ?? null)}`\n case 'assignCampaign':\n return `${quoted(step.campaignName || step.campaignId)}${standing(step.campaignId, step.campaignName, records?.campaigns ?? null)}`\n case 'runWorkflow':\n return `${quoted(step.workflowName || step.workflowId)}${standing(step.workflowId, step.workflowName, records?.workflows ?? null)}`\n case 'webhookPost':\n return `${quoted(step.webhookName || step.webhookId)}${standing(step.webhookId, step.webhookName, records?.webhooks ?? null)}`\n case 'datasetAppend':\n case 'updateDataset':\n return `${quoted(step.datasetName || step.datasetId)}${standing(step.datasetId, step.datasetName, records?.datasets ?? null)}`\n case 'siteAlert':\n return quoted(step.message)\n case 'wait':\n return durationOf(step.delayMinutes)\n case 'waitForEvent':\n return `${aiAutomationTriggerLabel(step.eventName)}, giving up after ${durationOf(step.timeoutMinutes)}`\n case 'exitFlow':\n return ''\n case 'setContactStage':\n return `\"${CONTACT_LIFECYCLE_STAGE_LABELS[step.lifecycleStage] ?? step.lifecycleStage}\"`\n case 'addContactTag':\n return quoted(step.tag)\n case 'assignContactOwner':\n return step.roundRobin ? 'by round robin' : 'a named teammate'\n case 'createCrmTask':\n return `${quoted(step.title)}, a ${step.kind}, due in ${step.dueInDays} ${step.dueInDays === 1 ? 'day' : 'days'}${\n step.assigneeUid || step.assigneeEmail ? ', for a named teammate' : ''\n }`\n case 'logCrmActivity':\n return `a ${step.kind}: ${quoted(step.body)}`\n case 'customEvent':\n return quoted(step.eventName)\n default:\n return CLIENT_ACTION_STEP_TYPES.has(step.type) ? 'runs in the visitor’s browser on the page' : ''\n }\n })()\n const guard = (step.when?.conditions ?? []).filter(Boolean)\n const only = guard.length\n ? ` Only if ${guard.map(conditionLine).join(step.when?.combinator === 'or' ? ' or ' : ' and ')}.`\n : ''\n return `${label}${detail ? `: ${detail}` : ''}.${only}`\n}\n\n/** Minutes as a person says them. */\nfunction durationOf(minutes: unknown): string {\n const value = Number(minutes)\n if (!Number.isFinite(value) || value <= 0) return 'no time'\n const units: Array<[number, string]> = [\n [60 * 24 * 7, 'week'],\n [60 * 24, 'day'],\n [60, 'hour'],\n [1, 'minute'],\n ]\n for (const [size, unit] of units) {\n if (value % size === 0) {\n const count = value / size\n return `${count} ${unit}${count === 1 ? '' : 's'}`\n }\n }\n return `${value} minutes`\n}\n\n/** A saved action as an outline. */\nexport function aiActionOutline(\n action: HostAction,\n records: AiAutomationRecords | null,\n): string {\n const lines = [\n `Automation: ${quoted(action.name)} — an action, switched ${action.enabled === false ? 'off' : 'on'}.`,\n `Starts when: ${aiAutomationTriggerLabel(String(action.trigger?.event ?? ''))}.`,\n ]\n const conditions = normalizeTriggerConditions(action.trigger)\n if (conditions.length) {\n lines.push(\n `Only when ${action.trigger?.combinator === 'or' ? 'any' : 'all'} of these hold: ${conditions\n .map(conditionLine)\n .join('; ')}.`,\n )\n }\n if (action.trigger?.filter?.trim()) {\n lines.push(`And only when this expression is true: ${quoted(action.trigger.filter)}.`)\n }\n lines.push('Steps, in order:')\n ;(action.steps ?? []).forEach((step, index) => {\n lines.push(`${index + 1}. ${stepLine(step, records)}`)\n })\n if (!(action.steps ?? []).length) lines.push('(none)')\n return lines.join('\\n')\n}\n\n/**\n * A saved workflow as the workflows plugin's `workflow` record index shares\n * it: its name, and the facts that index documents — the stored step list\n * (each a function call by `functionId`, `functionName`, `args` and\n * `resultName`), the scope name it returns, and its trigger.\n */\nexport interface AiIndexedWorkflow {\n name: string\n steps?: Array<{\n functionId?: string\n functionName?: string\n args?: string[]\n resultName?: string\n }>\n returnValue?: string | null\n trigger?: { event?: string; filter?: string } | null\n}\n\n/** A saved workflow — a pipeline of function calls — as an outline. */\nexport function aiWorkflowOutline(\n workflow: AiIndexedWorkflow,\n functions: readonly AiAutomationNamedRecord[] | null,\n): string {\n const lines = [`Workflow: ${quoted(workflow.name)} — a workflow, a list of function calls.`]\n if (workflow.trigger?.event) {\n const filter = workflow.trigger.filter?.trim()\n lines.push(\n `Starts when: ${aiAutomationTriggerLabel(workflow.trigger.event)}${\n filter ? `, only when this expression is true: ${quoted(filter)}` : ''\n }.`,\n )\n } else {\n lines.push('Starts when: it is called — by an automation, an inbound webhook or a computed variable.')\n }\n lines.push('Steps, in order:')\n ;(workflow.steps ?? []).forEach((step, index) => {\n const name = step.functionName || step.functionId || ''\n const exists = standing(step.functionId, step.functionName, functions)\n const args = (step.args ?? []).filter((arg) => String(arg ?? '').trim())\n lines.push(\n `${index + 1}. Runs the function ${quoted(name)}${exists}${\n args.length ? `, given ${args.map((arg) => quoted(arg)).join(', ')}` : ''\n }; its result is kept as ${quoted(step.resultName || `step${index + 1}`)}.`,\n )\n })\n if (!(workflow.steps ?? []).length) lines.push('(none)')\n if (workflow.returnValue?.trim()) lines.push(`Returns: ${quoted(workflow.returnValue)}.`)\n return lines.join('\\n')\n}\n\n/** What the run history recorded about one run, as far as an outline reads it. */\nexport interface AiRunRecord {\n result?: unknown\n trigger?: unknown\n summary?: unknown\n action?: unknown\n createdAt?: unknown\n}\n\nfunction whenOf(value: unknown): string | null {\n const toDate = (value as { toDate?: () => Date } | null)?.toDate\n const date =\n typeof toDate === 'function'\n ? toDate.call(value)\n : value instanceof Date\n ? value\n : null\n return date && Number.isFinite(date.getTime()) ? `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC` : null\n}\n\n/** The errors a run recorded: the prose after \"with errors:\", or a workflow's failure. */\nexport function aiRunErrors(run: AiRunRecord): string[] {\n const action = String(run.action ?? '')\n const listed = action.match(/with errors:\\s*(.+)$/)?.[1]\n if (listed) {\n return listed\n .split(/;\\s+/)\n .map((error) => error.trim())\n .filter(Boolean)\n .slice(0, AI_OUTLINE_RUN_ERRORS_MAX)\n }\n const failed = action.match(/failed on [^:]+:\\s*(.+)$/)?.[1] ?? (run.result === 'failed' ? String(run.summary ?? '') : '')\n return failed.trim() ? [failed.trim()] : []\n}\n\n/** A failed run as an outline: when, on what, what it did, and what went wrong. */\nexport function aiRunOutline(run: AiRunRecord): string {\n const when = whenOf(run.createdAt)\n const lines = [\n `The run that failed: on ${aiAutomationTriggerLabel(String(run.trigger ?? ''))}${when ? `, ${when}` : ''}.`,\n ]\n const summary = String(run.summary ?? '').trim()\n const errors = aiRunErrors(run)\n if (summary && !errors.includes(summary)) lines.push(`What it did: ${aiRedactPersonal(summary)}.`)\n lines.push('What went wrong:')\n lines.push(...(errors.length ? errors.map((error) => `- ${aiRedactPersonal(error)}`) : ['- (the run recorded no error)']))\n return lines.join('\\n')\n}\n\n/** An explanation as the text a person reads. */\nexport function aiWorkflowExplanationText(\n explanation: AiWorkflowExplanation,\n mode: 'explain' | 'diagnose',\n): string {\n const parts = [explanation.summary]\n if (explanation.points.length) {\n parts.push(\n [\n mode === 'diagnose' ? 'What happened:' : 'What it does:',\n ...explanation.points.map((point, index) => `${index + 1}. ${point}`),\n ].join('\\n'),\n )\n }\n if (explanation.suggestions.length) {\n parts.push(\n [\n mode === 'diagnose' ? 'How to fix it:' : 'Worth checking:',\n ...explanation.suggestions.map((suggestion) => `- ${suggestion}`),\n ].join('\\n'),\n )\n }\n return parts.join('\\n\\n')\n}\n"],"names":["CLIENT_ACTION_STEP_TYPES","HOST_ACTION_STEP_LABELS","normalizeTriggerConditions","automationPlaceholderIn","CONTACT_LIFECYCLE_STAGE_LABELS","aiAutomationTriggerLabel","EMAIL_ADDRESS","AI_OUTLINE_BODY_MAX_CHARS","AI_OUTLINE_VALUE_MAX_CHARS","AI_OUTLINE_RUN_ERRORS_MAX","aiRedactPersonal","text","replace","quoted","value","max","String","trim","length","slice","conditionLine","condition","field","op","stage","placeholder","AI_OUTLINE_RECORD_KINDS","enrollList","assignCampaign","runWorkflow","webhookPost","datasetAppend","updateDataset","aiActionRecordKinds","action","kinds","steps","map","step","type","Set","filter","kind","Boolean","standing","id","name","records","found","some","record","stepLine","label","detail","subject","body","toField","title","listName","listId","lists","campaignName","campaignId","campaigns","workflowName","workflowId","workflows","webhookName","webhookId","webhooks","datasetName","datasetId","datasets","message","durationOf","delayMinutes","eventName","timeoutMinutes","lifecycleStage","tag","roundRobin","dueInDays","assigneeUid","assigneeEmail","has","guard","when","conditions","only","join","combinator","minutes","Number","isFinite","units","size","unit","count","aiActionOutline","lines","enabled","trigger","event","push","forEach","index","aiWorkflowOutline","workflow","functions","functionName","functionId","exists","args","arg","resultName","returnValue","whenOf","toDate","date","call","Date","getTime","toISOString","aiRunErrors","run","listed","match","split","error","failed","result","summary","aiRunOutline","createdAt","errors","includes","aiWorkflowExplanationText","explanation","mode","parts","points","point","suggestions","suggestion"],"mappings":"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"}
@@ -115,10 +115,35 @@ export interface AiBuildPlanScreen {
115
115
  */
116
116
  id?: string;
117
117
  }
118
+ /** The third-party players a plan may embed: the hosts the Video embed element plays. */
119
+ export type AiBuildPlanEmbedHost = 'youtube' | 'vimeo';
120
+ export declare const AI_BUILD_PLAN_EMBED_HOSTS: readonly AiBuildPlanEmbedHost[];
121
+ /** What each host is called in a sentence. */
122
+ export declare const AI_BUILD_PLAN_EMBED_HOST_NAMES: Record<AiBuildPlanEmbedHost, string>;
123
+ /**
124
+ * A third-party player the brief asks for (rule 16, AGL-3433). It loads its
125
+ * host's code on the page that carries it, so it is planned, where the member
126
+ * reads that cost before confirming, and a build admits only what the
127
+ * confirmed plan lists.
128
+ */
129
+ export interface AiBuildPlanEmbed {
130
+ host: AiBuildPlanEmbedHost;
131
+ /** Where it plays: a screen of the plan by its slug, or `new:<name>` for a component the plan creates. */
132
+ where: string;
133
+ /** The brief's own words asking for it, which the plan step finds in the brief. */
134
+ asked: string;
135
+ /** The video's link as the brief gives it, or null for the site owner to paste. */
136
+ url: string | null;
137
+ }
118
138
  export interface AiBuildPlan {
119
139
  reuse: AiBuildPlanReuse[];
120
140
  create: AiBuildPlanCreate[];
121
141
  screens: AiBuildPlanScreen[];
142
+ /**
143
+ * The third-party players the brief asks for. Absent is none: a plan kept
144
+ * before the list existed, and every plan whose brief asks for no player.
145
+ */
146
+ embeds?: AiBuildPlanEmbed[];
122
147
  }
123
148
  /**
124
149
  * How much one plan may hold. A job builds what one proposal can describe
@@ -132,6 +157,7 @@ export declare const AI_BUILD_PLAN_LIMITS: {
132
157
  readonly sections: 16;
133
158
  readonly uses: 8;
134
159
  readonly fields: 30;
160
+ readonly embeds: 4;
135
161
  readonly text: 200;
136
162
  readonly seoDescription: 320;
137
163
  readonly items: 500;
@@ -150,6 +176,18 @@ export declare const AI_BUILD_PLAN_LIMITS: {
150
176
  * states, so naming the reader's here would contradict the rule.
151
177
  */
152
178
  export declare const AI_BUILD_PLAN_TOOL: AiTool;
179
+ /** Words that ask for a video, or a link to one. */
180
+ export declare const AI_PLAN_ASKS_FOR_VIDEO: RegExp;
181
+ /**
182
+ * The plan tool with its list of third-party players, offered only to a brief
183
+ * that asks for a video (AGL-3433). Every other plan's request is the plain
184
+ * tool, byte for byte, so the list costs nothing on the plans that cannot use
185
+ * it, and a model answering a brief that names no video has nowhere to plan a
186
+ * player it was never asked for.
187
+ */
188
+ export declare const AI_BUILD_PLAN_EMBEDS_TOOL: AiTool;
189
+ /** The plan tool a brief is answered with. */
190
+ export declare function aiBuildPlanToolFor(brief: string): AiTool;
153
191
  export type AiBuildPlanParse = {
154
192
  ok: true;
155
193
  plan: AiBuildPlan;
@@ -171,6 +209,25 @@ export declare function parseAiBuildPlan(input: unknown): AiBuildPlanParse;
171
209
  export declare function isAiPlanNewRef(ref: string | null | undefined): ref is string;
172
210
  /** The creation a `new:<name>` reference names, case-insensitively. */
173
211
  export declare function aiPlanCreateFor(plan: AiBuildPlan, ref: string): AiBuildPlanCreate | undefined;
212
+ /** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */
213
+ export declare function aiPlanSlugKey(slug: string): string;
214
+ /**
215
+ * The players the plan lists for one output (AGL-3433): a screen by its slug,
216
+ * or a creation by its name. A build admits these and no other.
217
+ */
218
+ export declare function aiPlanEmbedsFor(plan: Pick<AiBuildPlan, 'embeds'> | null | undefined, target: {
219
+ slug: string;
220
+ } | {
221
+ create: string;
222
+ }): AiBuildPlanEmbed[];
223
+ /** The host a video link plays from, or null for a link neither host serves. */
224
+ export declare function aiEmbedHostOf(url: string): AiBuildPlanEmbedHost | null;
225
+ /**
226
+ * One video however its link is spelled — `youtu.be/<id>`, a watch link, an
227
+ * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element
228
+ * reads a link; null for a link it cannot play.
229
+ */
230
+ export declare function aiEmbedVideoKey(url: string): string | null;
174
231
  /**
175
232
  * A `new:<name>` reference that names no entry of the plan's create list: a
176
233
  * screen's own layout or template, or what one of its sections places, with
@@ -13,7 +13,8 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ /**
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /**
17
18
  * The plan a generation job builds from (AGL-2935): what the job's plan step
18
19
  * (`jobs/ai-job-plan-step.ts`) returns, before a single node is generated.
19
20
  *
@@ -87,6 +88,14 @@ export const AI_BUILD_PLAN_CREATE_KINDS = [
87
88
  'dataset',
88
89
  'email'
89
90
  ];
91
+ export const AI_BUILD_PLAN_EMBED_HOSTS = [
92
+ 'youtube',
93
+ 'vimeo'
94
+ ];
95
+ /** What each host is called in a sentence. */ export const AI_BUILD_PLAN_EMBED_HOST_NAMES = {
96
+ youtube: 'YouTube',
97
+ vimeo: 'Vimeo'
98
+ };
90
99
  /**
91
100
  * How much one plan may hold. A job builds what one proposal can describe
92
101
  * and one person can read before confirming; a brief asking for more is
@@ -98,6 +107,7 @@ export const AI_BUILD_PLAN_CREATE_KINDS = [
98
107
  sections: 16,
99
108
  uses: 8,
100
109
  fields: 30,
110
+ embeds: 4,
101
111
  text: 200,
102
112
  seoDescription: 320,
103
113
  items: 500
@@ -143,7 +153,7 @@ const strings = (description)=>({
143
153
  * states, so naming the reader's here would contradict the rule.
144
154
  */ export const AI_BUILD_PLAN_TOOL = {
145
155
  name: 'submit_build_plan',
146
- description: 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the screens it builds from them.',
156
+ description: 'Submit the build plan: what the site already has that this job reuses, what it creates and why, and the pages it builds from them.',
147
157
  strict: true,
148
158
  inputSchema: {
149
159
  type: 'object',
@@ -206,7 +216,7 @@ const strings = (description)=>({
206
216
  },
207
217
  screens: {
208
218
  type: 'array',
209
- description: 'The screens the job builds, each a new draft.',
219
+ description: 'The pages the job builds, each a new draft.',
210
220
  items: {
211
221
  type: 'object',
212
222
  additionalProperties: false,
@@ -222,11 +232,11 @@ const strings = (description)=>({
222
232
  'sections'
223
233
  ],
224
234
  properties: {
225
- title: string(`The screen name, ${atMost()}.`),
235
+ title: string(`The page name, ${atMost()}.`),
226
236
  slug: string(`The path, ${atMost()}: lowercase words joined by hyphens, e.g. /services/roof-repair.`),
227
237
  layout: nullableString('The layout it renders inside: an inventory id or new:<name>.'),
228
238
  template: nullableString('The template it applies (inventory id or new:<name>), or null for a one-off page.'),
229
- duplicateOf: nullableString('The inventory screen it starts from as a duplicate, or null.'),
239
+ duplicateOf: nullableString('The inventory page it starts from as a duplicate, or null.'),
230
240
  nav: {
231
241
  type: 'boolean',
232
242
  description: 'Whether the site navigation gains an entry for it.'
@@ -260,6 +270,52 @@ const strings = (description)=>({
260
270
  }
261
271
  }
262
272
  };
273
+ /** Words that ask for a video, or a link to one. */ export const AI_PLAN_ASKS_FOR_VIDEO = /\b(?:videos?|films?|clips?|youtube|vimeo|player)\b|youtu\.be/i;
274
+ const EMBEDS_SCHEMA = {
275
+ type: 'array',
276
+ description: 'A YouTube or Vimeo player the brief asks for, on a screen or a created component. Empty unless the brief asks; a library film is a Video.',
277
+ items: {
278
+ type: 'object',
279
+ additionalProperties: false,
280
+ required: [
281
+ 'host',
282
+ 'where',
283
+ 'asked',
284
+ 'url'
285
+ ],
286
+ properties: {
287
+ host: {
288
+ type: 'string',
289
+ enum: [
290
+ ...AI_BUILD_PLAN_EMBED_HOSTS
291
+ ]
292
+ },
293
+ where: string('A screen slug, or new:<name> of a component.'),
294
+ asked: string('The words of the brief that ask for it, copied exactly.'),
295
+ url: nullableString('The video link the brief gives, or null.')
296
+ }
297
+ }
298
+ };
299
+ /**
300
+ * The plan tool with its list of third-party players, offered only to a brief
301
+ * that asks for a video (AGL-3433). Every other plan's request is the plain
302
+ * tool, byte for byte, so the list costs nothing on the plans that cannot use
303
+ * it, and a model answering a brief that names no video has nowhere to plan a
304
+ * player it was never asked for.
305
+ */ export const AI_BUILD_PLAN_EMBEDS_TOOL = _extends({}, AI_BUILD_PLAN_TOOL, {
306
+ inputSchema: _extends({}, AI_BUILD_PLAN_TOOL.inputSchema, {
307
+ required: [
308
+ ...AI_BUILD_PLAN_TOOL.inputSchema['required'],
309
+ 'embeds'
310
+ ],
311
+ properties: _extends({}, AI_BUILD_PLAN_TOOL.inputSchema['properties'], {
312
+ embeds: EMBEDS_SCHEMA
313
+ })
314
+ })
315
+ });
316
+ /** The plan tool a brief is answered with. */ export function aiBuildPlanToolFor(brief) {
317
+ return AI_PLAN_ASKS_FOR_VIDEO.test(brief) ? AI_BUILD_PLAN_EMBEDS_TOOL : AI_BUILD_PLAN_TOOL;
318
+ }
263
319
  function isRecord(value) {
264
320
  return typeof value === 'object' && value !== null && !Array.isArray(value);
265
321
  }
@@ -309,6 +365,7 @@ let PlanShapeError = class PlanShapeError extends Error {
309
365
  return value;
310
366
  };
311
367
  try {
368
+ var _root_embeds;
312
369
  const root = record(input, 'plan');
313
370
  const reuse = list(root['reuse'], 'reuse', AI_BUILD_PLAN_LIMITS.reuse).map((raw, index)=>{
314
371
  const entry = record(raw, `reuse[${index}]`);
@@ -369,13 +426,27 @@ let PlanShapeError = class PlanShapeError extends Error {
369
426
  })
370
427
  };
371
428
  });
429
+ const embeds = list((_root_embeds = root['embeds']) != null ? _root_embeds : [], 'embeds', AI_BUILD_PLAN_LIMITS.embeds).map((raw, index)=>{
430
+ const entry = record(raw, `embeds[${index}]`);
431
+ const where = text(entry['where'], `embeds[${index}].where`);
432
+ if (!where) throw new PlanShapeError(`embeds[${index}].where is empty`);
433
+ return {
434
+ host: oneOf(entry['host'], AI_BUILD_PLAN_EMBED_HOSTS, `embeds[${index}].host`),
435
+ where,
436
+ asked: text(entry['asked'], `embeds[${index}].asked`),
437
+ url: nullable(entry['url'], `embeds[${index}].url`)
438
+ };
439
+ });
440
+ // A plan whose brief asks for no player carries no list, as it did before the list existed.
372
441
  return {
373
442
  ok: true,
374
- plan: {
443
+ plan: _extends({
375
444
  reuse,
376
445
  create,
377
446
  screens
378
- },
447
+ }, embeds.length ? {
448
+ embeds
449
+ } : {}),
379
450
  repairs
380
451
  };
381
452
  } catch (error) {
@@ -394,6 +465,44 @@ let PlanShapeError = class PlanShapeError extends Error {
394
465
  const name = ref.slice(AI_PLAN_NEW_REF_PREFIX.length).trim().toLowerCase();
395
466
  return plan.create.find((entry)=>entry.name.toLowerCase() === name);
396
467
  }
468
+ /** A slug as two spellings of one path compare: lowercase, one leading slash, no trailing one. */ export function aiPlanSlugKey(slug) {
469
+ const path = slug.trim().toLowerCase().replace(/^\/*/, '/').replace(/\/+$/, '');
470
+ return path || '/';
471
+ }
472
+ /**
473
+ * The players the plan lists for one output (AGL-3433): a screen by its slug,
474
+ * or a creation by its name. A build admits these and no other.
475
+ */ export function aiPlanEmbedsFor(plan, target) {
476
+ var _ref;
477
+ const key = 'slug' in target ? aiPlanSlugKey(target.slug) : `${AI_PLAN_NEW_REF_PREFIX}${target.create.trim().toLowerCase()}`;
478
+ return ((_ref = plan == null ? void 0 : plan.embeds) != null ? _ref : []).filter((embed)=>'slug' in target ? !isAiPlanNewRef(embed.where) && aiPlanSlugKey(embed.where) === key : embed.where.trim().toLowerCase() === key);
479
+ }
480
+ /** The host a video link plays from, or null for a link neither host serves. */ export function aiEmbedHostOf(url) {
481
+ try {
482
+ const parsed = new URL(url.trim());
483
+ if (parsed.protocol !== 'https:') return null;
484
+ const host = parsed.hostname.replace(/^(?:www|m)\./, '');
485
+ if (host === 'youtu.be' || host === 'youtube.com' || host === 'youtube-nocookie.com') {
486
+ return 'youtube';
487
+ }
488
+ if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo';
489
+ } catch (unused) {
490
+ /* not a link */ }
491
+ return null;
492
+ }
493
+ /**
494
+ * One video however its link is spelled — `youtu.be/<id>`, a watch link, an
495
+ * embed or a Shorts link — as `<host>:<id>`, the way the Video embed element
496
+ * reads a link; null for a link it cannot play.
497
+ */ export function aiEmbedVideoKey(url) {
498
+ var _parsed_searchParams_get;
499
+ const host = aiEmbedHostOf(url);
500
+ if (!host) return null;
501
+ const parsed = new URL(url.trim());
502
+ const parts = parsed.pathname.split('/').filter(Boolean);
503
+ const id = host === 'vimeo' ? parts.find((part)=>/^\d{6,}$/.test(part)) : parsed.hostname.endsWith('youtu.be') ? parts[0] : (_parsed_searchParams_get = parsed.searchParams.get('v')) != null ? _parsed_searchParams_get : parts[0] === 'embed' || parts[0] === 'shorts' ? parts[1] : undefined;
504
+ return id && /^[\w-]{6,20}$/.test(id) ? `${host}:${id}` : null;
505
+ }
397
506
  /**
398
507
  * Every `new:<name>` reference the plan's create list does not carry, in plan
399
508
  * order: each screen's layout, then its template, then what its sections