@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/model/ai-page-job.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 AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport type { AiPlanUncreatable } from './ai-plan-capabilities'\n\n/**\n * What a page job is (AGL-2907): the page types a brief may name, the inputs\n * the job doors admit, and the plans a page generator can build.\n *\n * A page job builds ONE screen. It places the site's components by id, binds\n * its forms by id, and renders inside a layout — and since AGL-3031 it first\n * builds the layout, forms and components its own plan creates, in the same\n * job, through the steps that own them, so a doctrine-correct plan is one it\n * finishes rather than one it sends the member away from. Anything else a\n * plan creates — a template, a theme change, a dataset, an email — is another\n * job's work, and such a plan is refused before anything is spent, with a\n * sentence naming what to make first.\n *\n * This module imports nothing at runtime, so the doors, the step, the console\n * dialog and the specs read one vocabulary.\n */\n\nexport type AiPageType =\n | 'landing'\n | 'about'\n | 'pricing'\n | 'contact'\n | 'blogIndex'\n | 'product'\n | 'service'\n | 'event'\n\nexport interface AiPageTypeDefinition {\n id: AiPageType\n /** The chip's label in the Describe dialog. */\n label: string\n /** What the page is for, as the plan and the generation are told. */\n purpose: string\n}\n\nexport const AI_PAGE_TYPES: readonly AiPageTypeDefinition[] = [\n {\n id: 'landing',\n label: 'Landing',\n purpose: 'a landing page that makes one offer and asks for one action',\n },\n {\n id: 'about',\n label: 'About',\n purpose: 'an about page: who the business is, what it stands for and who is behind it',\n },\n {\n id: 'pricing',\n label: 'Pricing',\n purpose: 'a pricing page: the plans or packages, what each includes and how to start',\n },\n {\n id: 'contact',\n label: 'Contact',\n purpose: 'a contact page: how to reach the business, and where and when',\n },\n {\n id: 'blogIndex',\n label: 'Blog index',\n purpose: 'a blog index that lists the posts of the site’s content collection',\n },\n {\n id: 'product',\n label: 'Product',\n purpose: 'a page about one product: what it is, who it is for and how to get it',\n },\n {\n id: 'service',\n label: 'Service',\n purpose: 'a page about one service: what it covers, how it works and how to book it',\n },\n {\n id: 'event',\n label: 'Event',\n purpose: 'an event page: what it is, when and where, and how to attend',\n },\n]\n\nconst PAGE_TYPE_BY_ID = new Map(AI_PAGE_TYPES.map((type) => [type.id, type]))\n\n/** The definition of a page type id, or `null` for anything else. */\nexport function aiPageTypeDefinition(id: unknown): AiPageTypeDefinition | null {\n return typeof id === 'string' ? (PAGE_TYPE_BY_ID.get(id as AiPageType) ?? null) : null\n}\n\nexport interface AiPageJobInputs {\n /** The type the member picked; `null` when they picked none. */\n pageType: AiPageType | null\n}\n\n/** A page job's inputs, or the sentence the door refuses them with. */\nexport function parseAiPageJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiPageJobInputs | string {\n const raw = inputs?.['pageType']\n if (raw === undefined || raw === null || raw === '') return { pageType: null }\n const definition = aiPageTypeDefinition(raw)\n return definition\n ? { pageType: definition.id }\n : `pageType must be one of ${AI_PAGE_TYPES.map((type) => type.id).join(', ')}`\n}\n\n/** A creation a plan names, as the refusal lists it. */\nexport interface AiPagePrerequisite {\n kind: AiBuildPlanCreateKind\n name: string\n}\n\n/**\n * What the plan asks to be created that a page job does not build, in the\n * order the plan lists it, each named once: every entry is something a\n * member makes first.\n */\nexport function aiPagePlanPrerequisites(plan: AiBuildPlan): AiPagePrerequisite[] {\n const seen = new Set<string>()\n const prerequisites: AiPagePrerequisite[] = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_PAGE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * The creations a page job builds itself from its own plan (AGL-3031): the\n * layout the page renders inside, and the forms and components it places.\n * Each is built before the page, as a draft, by the step that builds that\n * kind on its own. The plan step is told this list before it answers, and\n * the plan rules refuse a creation outside it with the one re-ask every rule\n * gets, so a plan that needs another never reaches a member's Confirm.\n */\nexport const AI_PAGE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = ['layout', 'form', 'component']\n\n/**\n * Why a page job cannot build a plan of this SHAPE — no page, several pages,\n * a page with no sections — in a sentence a member reads; `null` when the\n * shape is one it builds. The plan step holds a plan to it with a re-ask, and\n * the doors hold a confirmed plan to it again through `aiPagePlanRefusal`.\n */\nexport function aiPagePlanShapeRefusal(plan: AiBuildPlan): string | null {\n if (plan.screens.length === 0) {\n return 'This plan has no page to build. Describe the page again.'\n }\n if (plan.screens.length > 1) {\n return `This plan builds ${plan.screens.length} pages, and a page job builds one. Describe each page on its own.`\n }\n if (!plan.screens[0].sections.length) {\n return 'This plan’s page has no sections to build. Describe the page again.'\n }\n return null\n}\n\n/**\n * Why a page job cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can. A page job builds exactly one screen,\n * from what the site already has.\n */\nexport function aiPagePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiPagePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiPagePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This page needs what the site does not have yet. Create ${listed}, then describe the page again.`\n}\n\n/**\n * Why a page job cannot build the creations its plan names ON THIS SITE NOW,\n * in a sentence a member reads when confirming it: what the workspace's plan\n * does not include, or what the site has no room left for. The plan step was\n * told what could be created, so this is the site changing between the plan\n * and its confirmation.\n */\nexport function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[]): string | null {\n if (!refused.length) return null\n const parts = refused.map(\n ({ kind, name, reason }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}”, because ${reason}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join('; ')}; and ${parts[parts.length - 1]}`\n return `This page cannot be built as planned: it creates ${listed}. Describe the page again.`\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_TYPES","id","label","purpose","PAGE_TYPE_BY_ID","Map","map","type","aiPageTypeDefinition","get","parseAiPageJobInputs","inputs","raw","undefined","pageType","definition","join","aiPagePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","key","toLowerCase","has","push","entry","create","AI_PAGE_CREATE_KINDS","includes","aiPagePlanShapeRefusal","screens","length","sections","aiPagePlanRefusal","shape","parts","noun","where","listed","slice","aiPageCreationRefusal","refused","reason"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-page-job.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 AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport type { AiPlanUncreatable } from './ai-plan-capabilities'\n\n/**\n * What a page job is (AGL-2907): the page types a brief may name, the inputs\n * the job doors admit, and the plans a page generator can build.\n *\n * A page job builds ONE screen. It places the site's components by id, binds\n * its forms by id, and renders inside a layout — and since AGL-3031 it first\n * builds the layout, forms and components its own plan creates, in the same\n * job, through the steps that own them, so a doctrine-correct plan is one it\n * finishes rather than one it sends the member away from. Anything else a\n * plan creates — a template, a theme change, a dataset, an email — is another\n * job's work, and such a plan is refused before anything is spent, with a\n * sentence naming what to make first.\n *\n * This module imports nothing at runtime, so the doors, the step, the console\n * dialog and the specs read one vocabulary.\n */\n\nexport type AiPageType =\n | 'landing'\n | 'about'\n | 'pricing'\n | 'contact'\n | 'blogIndex'\n | 'product'\n | 'service'\n | 'event'\n\nexport interface AiPageTypeDefinition {\n id: AiPageType\n /** The chip's label in the Describe dialog. */\n label: string\n /** What the page is for, as the plan and the generation are told. */\n purpose: string\n}\n\nexport const AI_PAGE_TYPES: readonly AiPageTypeDefinition[] = [\n {\n id: 'landing',\n label: 'Landing',\n purpose: 'a landing page that makes one offer and asks for one action',\n },\n {\n id: 'about',\n label: 'About',\n purpose: 'an about page: who the business is, what it stands for and who is behind it',\n },\n {\n id: 'pricing',\n label: 'Pricing',\n purpose: 'a pricing page: the plans or packages, what each includes and how to start',\n },\n {\n id: 'contact',\n label: 'Contact',\n purpose: 'a contact page: how to reach the business, and where and when',\n },\n {\n id: 'blogIndex',\n label: 'Blog index',\n purpose: 'a blog index that lists the posts of the site’s content collection',\n },\n {\n id: 'product',\n label: 'Product',\n purpose: 'a page about one product: what it is, who it is for and how to get it',\n },\n {\n id: 'service',\n label: 'Service',\n purpose: 'a page about one service: what it covers, how it works and how to book it',\n },\n {\n id: 'event',\n label: 'Event',\n purpose: 'an event page: what it is, when and where, and how to attend',\n },\n]\n\nconst PAGE_TYPE_BY_ID = new Map(AI_PAGE_TYPES.map((type) => [type.id, type]))\n\n/** The definition of a page type id, or `null` for anything else. */\nexport function aiPageTypeDefinition(id: unknown): AiPageTypeDefinition | null {\n return typeof id === 'string' ? (PAGE_TYPE_BY_ID.get(id as AiPageType) ?? null) : null\n}\n\nexport interface AiPageJobInputs {\n /** The type the member picked; `null` when they picked none. */\n pageType: AiPageType | null\n}\n\n/** A page job's inputs, or the sentence the door refuses them with. */\nexport function parseAiPageJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiPageJobInputs | string {\n const raw = inputs?.['pageType']\n if (raw === undefined || raw === null || raw === '') return { pageType: null }\n const definition = aiPageTypeDefinition(raw)\n return definition\n ? { pageType: definition.id }\n : `pageType must be one of ${AI_PAGE_TYPES.map((type) => type.id).join(', ')}`\n}\n\n/** A creation a plan names, as the refusal lists it. */\nexport interface AiPagePrerequisite {\n kind: AiBuildPlanCreateKind\n name: string\n}\n\n/**\n * What the plan asks to be created that a page job does not build, in the\n * order the plan lists it, each named once: every entry is something a\n * member makes first.\n */\nexport function aiPagePlanPrerequisites(plan: AiBuildPlan): AiPagePrerequisite[] {\n const seen = new Set<string>()\n const prerequisites: AiPagePrerequisite[] = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_PAGE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * The creations a page job builds itself from its own plan (AGL-3031): the\n * layout the page renders inside, and the forms and components it places.\n * Each is built before the page, as a draft, by the step that builds that\n * kind on its own. The plan step is told this list before it answers, and\n * the plan rules refuse a creation outside it with the one re-ask every rule\n * gets, so a plan that needs another never reaches a member's Confirm.\n */\nexport const AI_PAGE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = ['layout', 'form', 'component']\n\n/**\n * Why a page job cannot build a plan of this SHAPE — no page, several pages,\n * a page with no sections — in a sentence a member reads; `null` when the\n * shape is one it builds. The plan step holds a plan to it with a re-ask, and\n * the doors hold a confirmed plan to it again through `aiPagePlanRefusal`.\n */\nexport function aiPagePlanShapeRefusal(plan: AiBuildPlan): string | null {\n if (plan.screens.length === 0) {\n return 'This plan has no page to build. Describe the page again.'\n }\n if (plan.screens.length > 1) {\n return `This plan builds ${plan.screens.length} pages, and a page job builds one. Describe each page on its own.`\n }\n if (!plan.screens[0].sections.length) {\n return 'This plan’s page has no sections to build. Describe the page again.'\n }\n return null\n}\n\n/**\n * Why a page job cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can. A page job builds exactly one screen,\n * from what the site already has.\n */\nexport function aiPagePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiPagePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiPagePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This page needs what the site does not have yet. Create ${listed}, then describe the page again.`\n}\n\n/**\n * Why a page job cannot build the creations its plan names ON THIS SITE NOW,\n * in a sentence a member reads when confirming it: what the workspace's plan\n * does not include, or what the site has no room left for. The plan step was\n * told what could be created, so this is the site changing between the plan\n * and its confirmation. `noun` names what the job builds, for a template,\n * layout, component, form or email job held the same way (AGL-3143 §15).\n */\nexport function aiPageCreationRefusal(refused: readonly AiPlanUncreatable[], noun = 'page'): string | null {\n if (!refused.length) return null\n const parts = refused.map(\n ({ kind, name, reason }) => `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}”, because ${reason}`,\n )\n const listed =\n parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join('; ')}; and ${parts[parts.length - 1]}`\n return `This ${noun} cannot be built as planned: it creates ${listed}. Describe the ${noun} again.`\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_TYPES","id","label","purpose","PAGE_TYPE_BY_ID","Map","map","type","aiPageTypeDefinition","get","parseAiPageJobInputs","inputs","raw","undefined","pageType","definition","join","aiPagePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","key","toLowerCase","has","push","entry","create","AI_PAGE_CREATE_KINDS","includes","aiPagePlanShapeRefusal","screens","length","sections","aiPagePlanRefusal","shape","parts","noun","where","listed","slice","aiPageCreationRefusal","refused","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,4BAA4B,EAC5BC,oBAAoB,QAGf,qBAAiB;AAsCxB,OAAO,MAAMC,gBAAiD;IAC5D;QACEC,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,SAAS;IACX;CACD,CAAA;AAED,MAAMC,kBAAkB,IAAIC,IAAIL,cAAcM,GAAG,CAAC,CAACC,OAAS;QAACA,KAAKN,EAAE;QAAEM;KAAK;AAE3E,mEAAmE,GACnE,OAAO,SAASC,qBAAqBP,EAAW;QACbG;IAAjC,OAAO,OAAOH,OAAO,YAAYG,uBAAAA,gBAAgBK,GAAG,CAACR,eAApBG,uBAAyC,OAAQ;AACpF;AAOA,qEAAqE,GACrE,OAAO,SAASM,qBACdC,MAA4D;IAE5D,MAAMC,MAAMD,0BAAAA,MAAQ,CAAC,WAAW;IAChC,IAAIC,QAAQC,aAAaD,QAAQ,QAAQA,QAAQ,IAAI,OAAO;QAAEE,UAAU;IAAK;IAC7E,MAAMC,aAAaP,qBAAqBI;IACxC,OAAOG,aACH;QAAED,UAAUC,WAAWd,EAAE;IAAC,IAC1B,CAAC,wBAAwB,EAAED,cAAcM,GAAG,CAAC,CAACC,OAASA,KAAKN,EAAE,EAAEe,IAAI,CAAC,OAAO;AAClF;AAQA;;;;CAIC,GACD,OAAO,SAASC,wBAAwBC,IAAiB;IACvD,MAAMC,OAAO,IAAIC;IACjB,MAAMC,gBAAsC,EAAE;IAC9C,MAAMC,MAAM,CAACC,MAA6BC;QACxC,MAAMC,MAAM,GAAGF,KAAK,CAAC,EAAEC,KAAKE,WAAW,IAAI;QAC3C,IAAIP,KAAKQ,GAAG,CAACF,MAAM;QACnBN,KAAKG,GAAG,CAACG;QACTJ,cAAcO,IAAI,CAAC;YAAEL;YAAMC;QAAK;IAClC;IACA,KAAK,MAAMK,SAASX,KAAKY,MAAM,CAAE;QAC/B,IAAI,CAACC,qBAAqBC,QAAQ,CAACH,MAAMN,IAAI,GAAGD,IAAIO,MAAMN,IAAI,EAAEM,MAAML,IAAI;IAC5E;IACA,0EAA0E;IAC1E,2EAA2E;IAC3E,4EAA4E;IAC5E,oEAAoE;IACpE,yEAAyE;IACzE,KAAK,MAAM,EAAEA,IAAI,EAAE,IAAIzB,qBAAqBmB,MAAOI,IAAI,aAAaE;IACpE,OAAOH;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMU,uBAAyD;IAAC;IAAU;IAAQ;CAAY,CAAA;AAErG;;;;;CAKC,GACD,OAAO,SAASE,uBAAuBf,IAAiB;IACtD,IAAIA,KAAKgB,OAAO,CAACC,MAAM,KAAK,GAAG;QAC7B,OAAO;IACT;IACA,IAAIjB,KAAKgB,OAAO,CAACC,MAAM,GAAG,GAAG;QAC3B,OAAO,CAAC,iBAAiB,EAAEjB,KAAKgB,OAAO,CAACC,MAAM,CAAC,iEAAiE,CAAC;IACnH;IACA,IAAI,CAACjB,KAAKgB,OAAO,CAAC,EAAE,CAACE,QAAQ,CAACD,MAAM,EAAE;QACpC,OAAO;IACT;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,kBAAkBnB,IAAiB;IACjD,MAAMoB,QAAQL,uBAAuBf;IACrC,IAAIoB,OAAO,OAAOA;IAClB,MAAMjB,gBAAgBJ,wBAAwBC;IAC9C,IAAI,CAACG,cAAcc,MAAM,EAAE,OAAO;IAClC,MAAMI,QAAQlB,cAAcf,GAAG,CAC7B,CAAC,EAAEiB,IAAI,EAAEC,IAAI,EAAE,GAAK,CAAC,IAAI,EAAE1B,4BAA4B,CAACyB,KAAK,CAACiB,IAAI,CAAC,EAAE,EAAEhB,KAAK,EAAE,EAAE1B,4BAA4B,CAACyB,KAAK,CAACkB,KAAK,EAAE;IAE5H,MAAMC,SACJH,MAAMJ,MAAM,KAAK,IAAII,KAAK,CAAC,EAAE,GAAG,GAAGA,MAAMI,KAAK,CAAC,GAAG,CAAC,GAAG3B,IAAI,CAAC,MAAM,KAAK,EAAEuB,KAAK,CAACA,MAAMJ,MAAM,GAAG,EAAE,EAAE;IACnG,OAAO,CAAC,wDAAwD,EAAEO,OAAO,+BAA+B,CAAC;AAC3G;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,sBAAsBC,OAAqC,EAAEL,OAAO,MAAM;IACxF,IAAI,CAACK,QAAQV,MAAM,EAAE,OAAO;IAC5B,MAAMI,QAAQM,QAAQvC,GAAG,CACvB,CAAC,EAAEiB,IAAI,EAAEC,IAAI,EAAEsB,MAAM,EAAE,GAAK,CAAC,IAAI,EAAEhD,4BAA4B,CAACyB,KAAK,CAACiB,IAAI,CAAC,EAAE,EAAEhB,KAAK,WAAW,EAAEsB,QAAQ;IAE3G,MAAMJ,SACJH,MAAMJ,MAAM,KAAK,IAAII,KAAK,CAAC,EAAE,GAAG,GAAGA,MAAMI,KAAK,CAAC,GAAG,CAAC,GAAG3B,IAAI,CAAC,MAAM,MAAM,EAAEuB,KAAK,CAACA,MAAMJ,MAAM,GAAG,EAAE,EAAE;IACpG,OAAO,CAAC,KAAK,EAAEK,KAAK,wCAAwC,EAAEE,OAAO,eAAe,EAAEF,KAAK,OAAO,CAAC;AACrG"}
@@ -233,8 +233,10 @@ export declare function aiPlanCreditEstimate(plan: AiBuildPlan, options?: AiPlan
233
233
  /**
234
234
  * About what a job of this kind costs to build from its plan (AGL-3031): a
235
235
  * page job counts the layout, forms and components it builds before its
236
- * page, and every other kind counts what a scaffold would. The figure the
237
- * proposal shows beside Confirm, and the hold a confirmation takes.
236
+ * page; a template, layout, component, form or email job counts its own
237
+ * record and every creation it builds before it (AGL-3143 §15); and a
238
+ * scaffold counts what it builds. The figure the proposal shows beside
239
+ * Confirm, and the hold a confirmation takes.
238
240
  */
239
241
  export declare function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number;
240
242
  /**
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { AI_BUILD_PLAN_CREATION_NOUNS, aiPlanUndeclaredRefs } from "./ai-build-plan.js";
17
+ import { AI_JOB_CREATE_KINDS } from "./ai-job-creations.js";
17
18
  import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
18
19
  /**
19
20
  * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted
@@ -294,11 +295,14 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
294
295
  /**
295
296
  * About what a job of this kind costs to build from its plan (AGL-3031): a
296
297
  * page job counts the layout, forms and components it builds before its
297
- * page, and every other kind counts what a scaffold would. The figure the
298
- * proposal shows beside Confirm, and the hold a confirmation takes.
298
+ * page; a template, layout, component, form or email job counts its own
299
+ * record and every creation it builds before it (AGL-3143 §15); and a
300
+ * scaffold counts what it builds. The figure the proposal shows beside
301
+ * Confirm, and the hold a confirmation takes.
299
302
  */ export function aiJobPlanCreditEstimate(kind, plan) {
300
- return aiPlanCreditEstimate(plan, kind === 'page' ? {
301
- creates: AI_PAGE_CREATE_KINDS
303
+ const creates = kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind];
304
+ return aiPlanCreditEstimate(plan, creates ? {
305
+ creates
302
306
  } : {});
303
307
  }
304
308
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.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 AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page, and every other kind counts what a scaffold would. The figure the\n * proposal shows beside Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n return aiPlanCreditEstimate(plan, kind === 'page' ? { creates: AI_PAGE_CREATE_KINDS } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-job.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 AI_BUILD_PLAN_CREATION_NOUNS,\n aiPlanUndeclaredRefs,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\nimport { AI_JOB_CREATE_KINDS } from './ai-job-creations'\nimport { AI_PAGE_CREATE_KINDS } from './ai-page-job'\n\n/**\n * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted\n * with, the plans a scaffold can build, and what one is estimated to cost\n * before it starts.\n *\n * A scaffold is the one job kind that builds a whole site rather than one\n * record: a plan of four to eight screens, the layout they render inside,\n * the contact form they place, a palette suggestion, and — where the email\n * step is loaded — a welcome email. It builds each of those through the job\n * kind that already owns it, so this module describes what a scaffold may\n * ask for and refuses the rest at confirmation, before a credit is spent,\n * naming what to make first.\n *\n * ── The estimate is the guard rail ───────────────────────────────────────\n *\n * A scaffold is tens of model calls, where every other kind is a few, so the\n * member reads what it will cost before they confirm the plan that spends\n * it. The figure is the machine's own nominal credits per step multiplied by\n * the passes the plan implies — an estimate, and said to be one wherever it\n * is shown.\n *\n * This module imports nothing at runtime, so the doors, the step, the batch\n * card and the plan proposal read one vocabulary.\n */\n\n/** How many pages a scaffold builds: enough to be a site, few enough to read. */\nexport const AI_SITE_PAGES = { min: 4, max: 8 } as const\n\n/**\n * The most sections one scaffolded page may hold. The plan model allows more\n * for a page job, which builds one page; a scaffold builds eight, and each\n * section is a pass of its own.\n */\nexport const AI_SITE_MAX_SECTIONS = 8\n\n/**\n * Sections a page is assumed to hold before a plan names them, for the\n * estimate a member reads when they ask for a scaffold. A declared\n * assumption, not a measurement: the plan's own section counts replace it\n * the moment the plan exists.\n */\nexport const AI_SITE_NOMINAL_SECTIONS = 5\n\n/**\n * The credits one pass is held against: the machine's\n * `AI_JOB_STEP_RESERVE_CREDITS`, declared here so the estimate stays a pure\n * number the console computes without the Admin SDK. `ai-site-job.spec.ts`\n * holds the two equal.\n */\nexport const AI_SITE_PASS_CREDITS = 50\n\n/** The longest a scaffold's scalar inputs may run; they are names, not copy. */\nexport const AI_SITE_INPUT_MAX_CHARS = 120\n\n/** The sites one agency batch may generate for in one request. */\nexport const AI_SITE_BATCH_MAX = 25\n\n/**\n * The sites a plan must hold before the org Sites page offers the batch at\n * all: the Agency and Advanced bands, which the plan's `hostLimit` is what\n * distinguishes.\n */\nexport const AI_SITE_BATCH_MIN_HOST_LIMIT = 25\n\nexport interface AiSiteJobInputs {\n /** What the business is, in a few words: the brief's subject. */\n businessType: string\n /**\n * Who the site is for, in a few words (AGL-2918); empty when nobody said.\n * The plan step puts it in front of the model on a line of its own, as it\n * does every other scalar here, so it narrows the pages a plan proposes\n * without a prompt of its own.\n */\n audience: string\n /**\n * The starter site whose shape the person liked (AGL-2918), by its\n * `STARTER_TEMPLATES` id; empty when they picked none.\n *\n * A hint carried as the id rather than the starter, because an id is what\n * the guided start's list, this door and the plan's line all agree on\n * without any of them loading a catalog of node maps. Unrecognized ids are\n * admitted for the same reason an unrecognized business type is: it is a\n * few words in a brief, not a lookup.\n */\n starter: string\n /** How many pages the member asked for. */\n pages: number\n /** The per-site variables an agency batch varies; empty when none was given. */\n businessName: string\n city: string\n brand: string\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /**\n * Where the contact form's submissions go (AGL-2918); `null` where nobody\n * said, which is every job created before the question existed and every\n * door that does not ask it.\n *\n * The one setting a new site owner actually needs made, and the one thing\n * about a contact form a model cannot know: whether a submission is a note\n * to read or a lead to chase is a decision about how the business runs. So\n * an answer BINDS the form step rather than hinting to it, and `null`\n * leaves the model's own proposal standing, exactly as it stood before.\n */\n submissions: AiSiteSubmissions | null\n /** The batch this job was created under (AGL-2911); `null` for a single site. */\n batchId: string | null\n}\n\n/**\n * Where a site's contact form sends what a visitor writes.\n *\n * Two, not three. The form step's own vocabulary has a third — a mailing\n * list — which it can only answer with a note, because the stored routing has\n * no place for a list; offering it as a question would be asking somebody to\n * pick an outcome the platform then explains it cannot store.\n */\nexport const AI_SITE_SUBMISSIONS = ['inbox', 'lead'] as const\n\nexport type AiSiteSubmissions = (typeof AI_SITE_SUBMISSIONS)[number]\n\n/**\n * What each answer to \"where do submissions go\" is called, and what it means.\n *\n * Here rather than beside the guided start's own questions, because it is not\n * only the guided start that asks: the agency batch asks the same thing of a\n * whole run (AGL-2918), and two surfaces wording one decision two ways is two\n * decisions. `ai-site-job.spec.ts` holds the list to {@link AI_SITE_SUBMISSIONS}.\n */\nexport const AI_SITE_SUBMISSION_CHOICES: ReadonlyArray<{\n id: AiSiteSubmissions\n label: string\n blurb: string\n}> = [\n {\n id: 'inbox',\n label: 'The Inbox',\n blurb: 'Every message is one to read and reply to.',\n },\n {\n id: 'lead',\n label: 'The Inbox, and CRM as a lead',\n blurb: 'Every message with an email address is also somebody to follow up.',\n },\n]\n\n/**\n * Which kind of email a scaffold's email unit is (AGL-2918), carried on the\n * derived job as `inputs.emailType`. The email step names the kind on the one\n * line it puts above the brief, and a scaffold was naming none.\n *\n * A key of the email step's own `AI_EMAIL_TYPES`, held here because neither\n * step may import the other: a scaffold delegates by job kind, and a\n * deployment that scaffolds is not necessarily one that has loaded the email\n * step. `ai-job-email-step.spec.ts` holds this key to that catalog.\n *\n * `reply` and not `welcome`. The five kinds a member picks are campaigns\n * somebody sends to an audience, and this is an answer to one person who\n * wrote in through the site; the nearest, `welcome`, is a label about signing\n * up, and a kind that misdescribes the email is worse than an absent one\n * because it is the first thing the model is told.\n */\nexport const AI_SITE_EMAIL_TYPE = 'reply'\n\n/** Where a job's inputs say submissions go, or `null` where they do not say. */\nexport function aiSiteSubmissions(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSubmissions | null {\n const value = inputs?.['submissions']\n return (AI_SITE_SUBMISSIONS as readonly unknown[]).includes(value)\n ? (value as AiSiteSubmissions)\n : null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/\n\n/** A scaffold's inputs, or the sentence the door refuses them with. */\nexport function parseAiSiteJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteJobInputs | string {\n const text = (key: string): string | null => {\n const value = inputs?.[key]\n if (value === undefined || value === null || value === '') return ''\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > AI_SITE_INPUT_MAX_CHARS ? null : trimmed\n }\n const businessType = text('businessType')\n if (businessType === null) {\n return `businessType must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n if (!businessType) return 'Say what kind of business the site is for'\n const businessName = text('businessName')\n const city = text('city')\n const brand = text('brand')\n const audience = text('audience')\n const starter = text('starter')\n for (const [key, value] of [\n ['businessName', businessName],\n ['city', city],\n ['brand', brand],\n ['audience', audience],\n ['starter', starter],\n ] as const) {\n if (value === null)\n return `${key} must be text under ${AI_SITE_INPUT_MAX_CHARS} characters`\n }\n const rawPages = inputs?.['pages']\n const pages =\n typeof rawPages === 'number' ? rawPages : Number(rawPages ?? NaN)\n if (\n !Number.isInteger(pages) ||\n pages < AI_SITE_PAGES.min ||\n pages > AI_SITE_PAGES.max\n ) {\n return `pages must be a whole number from ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}`\n }\n // Where submissions go is admitted only as one of the two the form step\n // can actually bind; anything else is nobody having said, and the model\n // proposes as it always did rather than the door refusing the whole job.\n const submissions = aiSiteSubmissions(inputs)\n const rawBatch = inputs?.['batchId']\n const batchId =\n rawBatch === undefined || rawBatch === null || rawBatch === ''\n ? null\n : typeof rawBatch === 'string' && ID_CHARS.test(rawBatch)\n ? rawBatch\n : undefined\n if (batchId === undefined) return 'batchId is not a batch id'\n return {\n businessType,\n audience: audience as string,\n starter: starter as string,\n pages,\n businessName: businessName as string,\n city: city as string,\n brand: brand as string,\n welcomeEmail: inputs?.['welcomeEmail'] !== false,\n submissions,\n batchId,\n }\n}\n\n/**\n * What the site itself is, as a step that is NOT the scaffold reads it off a\n * job's inputs (AGL-2918).\n *\n * A scaffold delegates unit by unit under a job derived from its own — a page\n * job, a form job — and the derived job carries the scaffold's inputs. So the\n * two answers that describe the site rather than one record of it are\n * readable by every delegated step, and a step that wants them does not have\n * to know it was delegated to.\n *\n * Lenient where {@link parseAiSiteJobInputs} is strict, because it reads the\n * inputs of jobs that are not scaffolds: a page job started from the Screens\n * page has no `businessType` and no `pages`, and that is not an error here —\n * it is a site nobody described, and the answer is two empty strings.\n */\nexport interface AiSiteWords {\n /** What kind of site it is; empty when the job's inputs do not say. */\n about: string\n /** Who it is for; empty when the job's inputs do not say. */\n audience: string\n}\n\n/** Whether either half of {@link AiSiteWords} says anything. */\nexport function aiSiteWordsSaidAnything(words: AiSiteWords): boolean {\n return Boolean(words.about || words.audience)\n}\n\n/** What a job's inputs say the site is, read defensively and trimmed to the input ceiling. */\nexport function aiSiteWords(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteWords {\n const read = (key: string): string => {\n const value = inputs?.[key]\n if (typeof value !== 'string') return ''\n return value.replace(/\\s+/g, ' ').trim().slice(0, AI_SITE_INPUT_MAX_CHARS).trim()\n }\n return { about: read('businessType'), audience: read('audience') }\n}\n\n/**\n * What a scaffold builds for itself: the layout its pages render inside, the\n * form they place, and the palette suggestion a member applies in the Theme\n * section. Anything else a plan asks to create is a job of its own.\n */\nexport const AI_SITE_CREATE_KINDS: readonly AiBuildPlanCreateKind[] = [\n 'layout',\n 'form',\n 'theme-change',\n]\n\n/** The creations a scaffold cannot build, in the order the plan lists them, each named once. */\nexport function aiSitePlanPrerequisites(\n plan: AiBuildPlan,\n): Array<{ kind: AiBuildPlanCreateKind; name: string }> {\n const seen = new Set<string>()\n const prerequisites: Array<{ kind: AiBuildPlanCreateKind; name: string }> = []\n const add = (kind: AiBuildPlanCreateKind, name: string) => {\n const key = `${kind}:${name.toLowerCase()}`\n if (seen.has(key)) return\n seen.add(key)\n prerequisites.push({ kind, name })\n }\n for (const entry of plan.create) {\n if (!AI_SITE_CREATE_KINDS.includes(entry.kind)) add(entry.kind, entry.name)\n }\n // A reference the create list does not carry is refused by the plan rules\n // before a plan is kept: rule 2 for a screen's layout, rule 7 for anything\n // else (`plan-creation-undeclared`, AGL-3040). A plan confirmed before that\n // rule existed can still carry one. It is named as a component: the\n // reference carries no kind, and the refusal must say where one is made.\n for (const { name } of aiPlanUndeclaredRefs(plan)) add('component', name)\n return prerequisites\n}\n\n/**\n * Why a scaffold cannot build a plan of this SHAPE — the page band, a page's\n * sections, its addresses, its navigation — in a sentence a member reads;\n * `null` when the shape is one it builds. The plan step holds a plan to it\n * with a re-ask (AGL-3030), and the doors hold a confirmed plan to it again\n * through `aiSitePlanRefusal`.\n */\nexport function aiSitePlanShapeRefusal(plan: AiBuildPlan): string | null {\n const { screens } = plan\n if (\n screens.length < AI_SITE_PAGES.min ||\n screens.length > AI_SITE_PAGES.max\n ) {\n return `This plan builds ${screens.length} ${screens.length === 1 ? 'page' : 'pages'}, and a site scaffold builds ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.max}. Describe the site again.`\n }\n const slugs = new Set<string>()\n for (const screen of screens) {\n if (!screen.sections.length) {\n return `The page “${screen.title}” has no sections to build. Describe the site again.`\n }\n if (screen.sections.length > AI_SITE_MAX_SECTIONS) {\n return `The page “${screen.title}” has ${screen.sections.length} sections, and a scaffolded page holds ${AI_SITE_MAX_SECTIONS}. Describe the site again.`\n }\n const slug = screen.slug.trim().toLowerCase()\n if (slugs.has(slug)) {\n return `Two pages in this plan share the address ${screen.slug}. Describe the site again.`\n }\n slugs.add(slug)\n }\n if (!screens.some((screen) => screen.nav)) {\n return 'No page in this plan is in the site navigation. Describe the site again.'\n }\n return null\n}\n\n/**\n * Why a scaffold cannot build this plan, in a sentence a member reads when\n * confirming it; `null` when it can.\n */\nexport function aiSitePlanRefusal(plan: AiBuildPlan): string | null {\n const shape = aiSitePlanShapeRefusal(plan)\n if (shape) return shape\n const prerequisites = aiSitePlanPrerequisites(plan)\n if (!prerequisites.length) return null\n const parts = prerequisites.map(\n ({ kind, name }) =>\n `the ${AI_BUILD_PLAN_CREATION_NOUNS[kind].noun} “${name}” ${AI_BUILD_PLAN_CREATION_NOUNS[kind].where}`,\n )\n const listed =\n parts.length === 1\n ? parts[0]\n : `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n return `This site needs what the workspace does not have yet. Create ${listed}, then describe the site again.`\n}\n\n/** What the estimate counts a plan in: whether a welcome email follows, and the creations the job builds. */\nexport interface AiPlanPassOptions {\n welcomeEmail?: boolean\n /** The creation kinds the job builds itself; a scaffold's when absent. */\n creates?: readonly AiBuildPlanCreateKind[]\n}\n\n/**\n * The passes a plan implies: one per section of every screen, one more per\n * screen for its search listing and its draft, and one for each thing the\n * plan creates that the job builds. What the estimate is counted in.\n */\nexport function aiPlanPasses(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n const screens = plan.screens.reduce(\n (total, screen) => total + screen.sections.length + 1,\n 0,\n )\n const creates = options.creates ?? AI_SITE_CREATE_KINDS\n const creations = plan.create.filter((entry) => creates.includes(entry.kind)).length\n return screens + creations + (options.welcomeEmail ? 1 : 0)\n}\n\n/**\n * About what a plan costs to build, in credits. An ESTIMATE: the nominal\n * credits the machine holds per step, times the passes the plan implies.\n * What each pass actually costs is its model's tokens, recorded on the job\n * as it runs.\n */\nexport function aiPlanCreditEstimate(plan: AiBuildPlan, options: AiPlanPassOptions = {}): number {\n return aiPlanPasses(plan, options) * AI_SITE_PASS_CREDITS\n}\n\n/**\n * About what a job of this kind costs to build from its plan (AGL-3031): a\n * page job counts the layout, forms and components it builds before its\n * page; a template, layout, component, form or email job counts its own\n * record and every creation it builds before it (AGL-3143 §15); and a\n * scaffold counts what it builds. The figure the proposal shows beside\n * Confirm, and the hold a confirmation takes.\n */\nexport function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan): number {\n const creates =\n kind === 'page' ? AI_PAGE_CREATE_KINDS : AI_JOB_CREATE_KINDS[kind as keyof typeof AI_JOB_CREATE_KINDS]\n return aiPlanCreditEstimate(plan, creates ? { creates } : {})\n}\n\n/**\n * About what a scaffold of this many pages costs, before a plan names its\n * sections: the same arithmetic on the nominal section count, plus the plan\n * step that proposes it. Shown where a member asks for a scaffold.\n */\nexport function aiSiteCreditEstimate(\n pages: number,\n options: { welcomeEmail?: boolean } = {},\n): number {\n const screens = Math.max(0, Math.floor(pages))\n const passes =\n 1 +\n screens * (AI_SITE_NOMINAL_SECTIONS + 1) +\n 2 +\n (options.welcomeEmail ? 1 : 0)\n return passes * AI_SITE_PASS_CREDITS\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","aiPlanUndeclaredRefs","AI_JOB_CREATE_KINDS","AI_PAGE_CREATE_KINDS","AI_SITE_PAGES","min","max","AI_SITE_MAX_SECTIONS","AI_SITE_NOMINAL_SECTIONS","AI_SITE_PASS_CREDITS","AI_SITE_INPUT_MAX_CHARS","AI_SITE_BATCH_MAX","AI_SITE_BATCH_MIN_HOST_LIMIT","AI_SITE_SUBMISSIONS","AI_SITE_SUBMISSION_CHOICES","id","label","blurb","AI_SITE_EMAIL_TYPE","aiSiteSubmissions","inputs","value","includes","ID_CHARS","parseAiSiteJobInputs","text","key","undefined","trimmed","trim","length","businessType","businessName","city","brand","audience","starter","rawPages","pages","Number","NaN","isInteger","submissions","rawBatch","batchId","test","welcomeEmail","aiSiteWordsSaidAnything","words","Boolean","about","aiSiteWords","read","replace","slice","AI_SITE_CREATE_KINDS","aiSitePlanPrerequisites","plan","seen","Set","prerequisites","add","kind","name","toLowerCase","has","push","entry","create","aiSitePlanShapeRefusal","screens","slugs","screen","sections","title","slug","some","nav","aiSitePlanRefusal","shape","parts","map","noun","where","listed","join","aiPlanPasses","options","reduce","total","creates","creations","filter","aiPlanCreditEstimate","aiJobPlanCreditEstimate","aiSiteCreditEstimate","Math","floor","passes"],"mappings":"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"}
@@ -72,6 +72,16 @@ export declare function aiBindingTokensIn(value: string): string[];
72
72
  export declare function isAiAddressToken(token: string): boolean;
73
73
  /** The tokens of a subject a link or media prop may hold. */
74
74
  export declare function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[];
75
+ /**
76
+ * Whether a token is filled for a machine and never read on the page: a URL
77
+ * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
78
+ * or the ISO timestamp a Video element's publication date takes
79
+ * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown
80
+ * one sees hyphens and digits (AGL-3143 §16).
81
+ */
82
+ export declare function isAiUnreadToken(token: string): boolean;
83
+ /** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */
84
+ export declare function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[];
75
85
  /** What a template job reads from its inputs. */
76
86
  export interface AiTemplateJobInputs {
77
87
  subject: AiTemplateSubject;
@@ -111,6 +111,20 @@ export const AI_TEMPLATE_SUBJECT_DEFINITIONS = {
111
111
  /** The tokens of a subject a link or media prop may hold. */ export function aiTemplateAddressTokens(definition) {
112
112
  return definition.tokens.map((entry)=>entry.token).filter(isAiAddressToken);
113
113
  }
114
+ /**
115
+ * Whether a token is filled for a machine and never read on the page: a URL
116
+ * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)
117
+ * or the ISO timestamp a Video element's publication date takes
118
+ * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown
119
+ * one sees hyphens and digits (AGL-3143 §16).
120
+ */ export function isAiUnreadToken(token) {
121
+ var _token_replace_split_pop;
122
+ const field = (_token_replace_split_pop = token.replace(/[{}\s]/g, '').split('.').pop()) != null ? _token_replace_split_pop : '';
123
+ return /^(?:slug|publishedAt)$|Slug$/.test(field);
124
+ }
125
+ /** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */ export function aiTemplateShownTokens(definition) {
126
+ return definition.tokens.map((entry)=>entry.token).filter((token)=>!isAiUnreadToken(token));
127
+ }
114
128
  const RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/;
115
129
  /** The inputs of a template job, or a sentence naming what is missing. */ export function parseAiTemplateJobInputs(inputs) {
116
130
  var _ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.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 AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,oBAAoB,EACpBC,wBAAwB,EACxBC,mBAAmB,QAEd,+CAA8C;AAuBrD,OAAO,MAAMC,uBAAqD;IAAC;IAAS;IAAW;CAAS,CAAA;AAEhG;;;;;CAKC,GACD,OAAO,MAAMC,6BAAkE;IAC7E;QAAEC,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QAAEF,OAAO;QAAqBC,OAAO;QAASC,aAAa;IAA6B;IACxF;QAAEF,OAAO;QAAoBC,OAAO;QAAQC,aAAa;IAA6B;CACvF,CAAA;AAED,gFAAgF,GAChF,MAAMC,oBAAoBP,yBAAyBQ,MAAM,CAAC,CAACC,QACzDA,MAAML,KAAK,CAACM,UAAU,CAAC;AAuBzB,OAAO,MAAMC,kCAET;IACFF,OAAO;QACLG,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAId;eAAwBD;SAAyB;QAC7DgB,QAAQ;YAAC;YAAuB;YAAuB;YAAyB;SAAoB;QACpGC,aAAa;QACbC,WAAW;IACb;IACAC,SAAS;QACPP,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQZ;QACRa,QAAQ,EAAE;QACVC,aAAa;QACbC,WAAW;IACb;IACAE,QAAQ;QACNR,SAAS;QACTC,MAAM;QACNC,YAAY;QACZC,QAAQ;eAAIhB;eAAyBQ;SAAkB;QACvDS,QAAQ;YAAC;SAAuB;QAChCC,aAAa;QACbC,WAAW;IACb;AACF,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASG,kBAAkBC,KAAa;IAC7C,OAAO;WAAIA,MAAMC,QAAQ,CAAC;KAA6B,CAACC,GAAG,CAAC,CAACC,QAAU,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiBtB,KAAa;QAC9BA;IAAd,MAAMuB,SAAQvB,2BAAAA,MAAMwB,OAAO,CAAC,gBAAgB,IAAIC,KAAK,CAAC,KAAKC,GAAG,cAAhD1B,2BAAsD;IACpE,OAAO,uCAAuC2B,IAAI,CAACJ;AACrD;AAEA,2DAA2D,GAC3D,OAAO,SAASK,wBAAwBC,UAAuC;IAC7E,OAAOA,WAAWlB,MAAM,CAACS,GAAG,CAAC,CAACf,QAAUA,MAAML,KAAK,EAAEI,MAAM,CAACkB;AAC9D;AASA,MAAMQ,YAAY;AAElB,wEAAwE,GACxE,OAAO,SAASC,yBACdC,MAA4D;;IAE5D,MAAMxB,UAAUyB,eAAOD,0BAAAA,MAAQ,CAAC,UAAU,mBAAI;IAC9C,IAAI,CAAC,AAAClC,qBAA2CoC,QAAQ,CAAC1B,UAAU;QAClE,OAAO,CAAC,wDAAwD,EAAEV,qBAAqBqC,IAAI,CAAC,OAAO;IACrG;IACA,IAAI3B,YAAY,SAAS,OAAO;QAAEA,SAASA;QAA8B4B,cAAc;IAAK;IAC5F,MAAMC,MAAML,0BAAAA,MAAQ,CAAC,eAAe;IACpC,MAAMI,eAAe,OAAOC,QAAQ,WAAWA,IAAIC,IAAI,KAAK;IAC5D,IAAI,CAACR,UAAUH,IAAI,CAACS,eAAe;QACjC,OAAO;IACT;IACA,OAAO;QAAE5B,SAAS;QAAS4B;IAAa;AAC1C"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-subjects.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 AUTHOR_TOKEN_CATALOG,\n COLLECTION_TOKEN_CATALOG,\n ENTRY_TOKEN_CATALOG,\n type BindingTokenCatalogEntry,\n} from '@aglyn/aglyn/app-utils/binding-token-catalog'\n\n/**\n * What a generated page template renders for (AGL-2909): one page drawn once\n * per record the site already keeps.\n *\n * - `entry` — a content collection's entry page: a blog post, an event, a\n * case study. Picked as the collection's entry template screen.\n * - `product` — the store's product page. Picked as the product template in\n * Store settings.\n * - `author` — an author's page. Picked as the author page screen.\n *\n * Everything that differs per record is a binding token, and the tokens a\n * template may bind are the ones the platform fills on that page. For an\n * entry and an author those are the besigner's own insert-picker catalogs, so\n * what a model may bind and what an author can pick are one list; a product's\n * are the commerce product page's, which no catalog names yet (below).\n *\n * Model only: shapes, the vocabularies and the inputs reader, no runtime.\n */\n\nexport type AiTemplateSubject = 'entry' | 'product' | 'author'\n\nexport const AI_TEMPLATE_SUBJECTS: readonly AiTemplateSubject[] = ['entry', 'product', 'author']\n\n/**\n * The tokens the commerce product page fills. The commerce plugin's page\n * resolver is their definition of record; a spec reads its source and holds\n * this list to it in both directions, because a plugin never imports another\n * plugin's internals.\n */\nexport const AI_PRODUCT_TEMPLATE_TOKENS: readonly BindingTokenCatalogEntry[] = [\n { token: '{{product.name}}', label: 'Name', description: 'The product’s name.' },\n {\n token: '{{product.description}}',\n label: 'Description',\n description: 'The product’s description.',\n },\n {\n token: '{{product.price}}',\n label: 'Price',\n description: 'The price, or “From” the lowest price when variants differ.',\n },\n { token: '{{product.image}}', label: 'Image', description: 'The product’s first image.' },\n { token: '{{product.slug}}', label: 'Slug', description: 'The product’s URL segment.' },\n]\n\n/** The pager tokens, which resolve on an author's page over their own archive. */\nconst PAGINATION_TOKENS = COLLECTION_TOKEN_CATALOG.filter((entry) =>\n entry.token.startsWith('{{pagination.'),\n)\n\nexport interface AiTemplateSubjectDefinition {\n subject: AiTemplateSubject\n /** One rendered page's record, as a sentence names it. */\n noun: string\n /** The token the page's one h1 binds, so every record shows its own title. */\n titleToken: string\n /** Every token the page may bind. */\n tokens: readonly BindingTokenCatalogEntry[]\n /**\n * Palette blocks that fill themselves from this subject on its page, and\n * render nothing on another subject's. Inside a Collection Entries card each\n * fills from that card's entry instead, so a card is exempt.\n */\n blocks: readonly string[]\n /** The template's name when the plan names none. */\n defaultName: string\n /** Where a member puts the page to work, for the proposal and the docs. */\n appliedIn: string\n}\n\nexport const AI_TEMPLATE_SUBJECT_DEFINITIONS: Readonly<\n Record<AiTemplateSubject, AiTemplateSubjectDefinition>\n> = {\n entry: {\n subject: 'entry',\n noun: 'a collection entry',\n titleToken: '{{entry.title}}',\n tokens: [...ENTRY_TOKEN_CATALOG, ...COLLECTION_TOKEN_CATALOG],\n blocks: ['collectionEntryMeta', 'collectionEntryBody', 'collectionEntryAuthor', 'collectionRelated'],\n defaultName: 'Entry page template',\n appliedIn: 'the collection’s entry template page, under Template pages in Content',\n },\n product: {\n subject: 'product',\n noun: 'a product',\n titleToken: '{{product.name}}',\n tokens: AI_PRODUCT_TEMPLATE_TOKENS,\n blocks: [],\n defaultName: 'Product page template',\n appliedIn: 'the product page template in Store settings',\n },\n author: {\n subject: 'author',\n noun: 'an author',\n titleToken: '{{author.name}}',\n tokens: [...AUTHOR_TOKEN_CATALOG, ...PAGINATION_TOKENS],\n blocks: ['contentAuthorProfile'],\n defaultName: 'Author page template',\n appliedIn: 'the author page on the Authors tab',\n },\n}\n\n/** Every `{{…}}` token a string holds, spelled without inner whitespace. */\nexport function aiBindingTokensIn(value: string): string[] {\n return [...value.matchAll(/\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g)].map((match) => `{{${match[1]}}}`)\n}\n\n/**\n * Whether a token may fill a link or media prop whole: its field is an\n * address or a picture (`{{entry.url}}`, `{{entry.coverImage}}`,\n * `{{author.image}}`), never copy.\n */\nexport function isAiAddressToken(token: string): boolean {\n const field = token.replace(/^\\{\\{|\\}\\}$/g, '').split('.').pop() ?? ''\n return /^(?:url|image)$|(?:Url|Image|Video)$/.test(field)\n}\n\n/** The tokens of a subject a link or media prop may hold. */\nexport function aiTemplateAddressTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter(isAiAddressToken)\n}\n\n/**\n * Whether a token is filled for a machine and never read on the page: a URL\n * segment (`{{entry.slug}}`, `{{collection.slug}}`, `{{entry.collectionSlug}}`)\n * or the ISO timestamp a Video element's publication date takes\n * (`{{entry.publishedAt}}`). A block may be handed one whole; a reader shown\n * one sees hyphens and digits (AGL-3143 §16).\n */\nexport function isAiUnreadToken(token: string): boolean {\n const field = token.replace(/[{}\\s]/g, '').split('.').pop() ?? ''\n return /^(?:slug|publishedAt)$|Slug$/.test(field)\n}\n\n/** The tokens of a subject its page shows: every one a reader reads or follows, which is all but the unread. */\nexport function aiTemplateShownTokens(definition: AiTemplateSubjectDefinition): string[] {\n return definition.tokens.map((entry) => entry.token).filter((token) => !isAiUnreadToken(token))\n}\n\n/** What a template job reads from its inputs. */\nexport interface AiTemplateJobInputs {\n subject: AiTemplateSubject\n /** The content collection an entry template is for; `null` for a product or author page. */\n collectionId: string | null\n}\n\nconst RECORD_ID = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The inputs of a template job, or a sentence naming what is missing. */\nexport function parseAiTemplateJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiTemplateJobInputs | string {\n const subject = String(inputs?.['subject'] ?? '')\n if (!(AI_TEMPLATE_SUBJECTS as readonly string[]).includes(subject)) {\n return `Name what the template is for: inputs.subject is one of ${AI_TEMPLATE_SUBJECTS.join(', ')}`\n }\n if (subject !== 'entry') return { subject: subject as AiTemplateSubject, collectionId: null }\n const raw = inputs?.['collectionId']\n const collectionId = typeof raw === 'string' ? raw.trim() : ''\n if (!RECORD_ID.test(collectionId)) {\n return 'An entry page template names its content collection in inputs.collectionId'\n }\n return { subject: 'entry', collectionId }\n}\n"],"names":["AUTHOR_TOKEN_CATALOG","COLLECTION_TOKEN_CATALOG","ENTRY_TOKEN_CATALOG","AI_TEMPLATE_SUBJECTS","AI_PRODUCT_TEMPLATE_TOKENS","token","label","description","PAGINATION_TOKENS","filter","entry","startsWith","AI_TEMPLATE_SUBJECT_DEFINITIONS","subject","noun","titleToken","tokens","blocks","defaultName","appliedIn","product","author","aiBindingTokensIn","value","matchAll","map","match","isAiAddressToken","field","replace","split","pop","test","aiTemplateAddressTokens","definition","isAiUnreadToken","aiTemplateShownTokens","RECORD_ID","parseAiTemplateJobInputs","inputs","String","includes","join","collectionId","raw","trim"],"mappings":"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"}
@@ -13,7 +13,7 @@
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
- */ import { assistProviderCostUsd, ASSIST_PROVIDER_COST_FIELD } from "@aglyn/aglyn/app-utils/assist-credits";
16
+ */ import { assistProviderCostUsd, ASSIST_PROVIDER_COST_FIELD } from "../usage/assist-credits.js";
17
17
  /**
18
18
  * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.
19
19
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-tokens.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 assistProviderCostUsd,\n ASSIST_PROVIDER_COST_FIELD,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport type { AiUsage } from '../providers/contract'\n\n/**\n * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.\n *\n * A provider reports four counts per request, and each is priced at its own\n * rate: prompt tokens sent uncached, prompt tokens read from the prompt\n * cache, prompt tokens written to it, and tokens generated. The org month\n * (per kind), the person's month and the job step keep the same four, so a\n * figure read off one means what it means on the others, and the cache hit\n * rate is the same division everywhere it is shown.\n *\n * Stored shapes:\n *\n * orgs/{orgId}/assistUsage/{YYYY-MM}\n * kinds: { [kind]: { requests, estCostUsd, providerCostUsd,\n * tokens: { input, cached, cacheWrite, output } } }\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * tokens: { input, cached, cacheWrite, output }\n *\n * The month document's own top-level `inputTokens`, `outputTokens`,\n * `cacheReadTokens` and `cacheWriteTokens` stay its totals; `kinds` splits\n * them by what the request was for.\n */\n\n/** The month document's per-kind map. */\nexport const AI_USAGE_MONTH_KINDS_FIELD = 'kinds'\n\n/** The token map, on a person's month and on each kind of the org's. */\nexport const AI_USAGE_TOKENS_FIELD = 'tokens'\n\nexport interface AiTokenTotals {\n /** Prompt tokens sent uncached, at the model's input rate. */\n input: number\n /** Prompt tokens read from the prompt cache, at its read rate. */\n cached: number\n /** Prompt tokens written to the prompt cache, at its write rate. */\n cacheWrite: number\n /** Tokens the model generated, thinking included. */\n output: number\n}\n\nconst count = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport function emptyAiTokenTotals(): AiTokenTotals {\n return { input: 0, cached: 0, cacheWrite: 0, output: 0 }\n}\n\n/** A request's usage, in the rollups' names. */\nexport function aiTokenTotalsFrom(usage: AiUsage): AiTokenTotals {\n return {\n input: count(usage.inputTokens),\n cached: count(usage.cacheReadTokens),\n cacheWrite: count(usage.cacheWriteTokens),\n output: count(usage.outputTokens),\n }\n}\n\nexport function addAiTokenTotals(a: AiTokenTotals, b: AiTokenTotals): AiTokenTotals {\n return {\n input: a.input + b.input,\n cached: a.cached + b.cached,\n cacheWrite: a.cacheWrite + b.cacheWrite,\n output: a.output + b.output,\n }\n}\n\n/** A stored token map read back, every absent or malformed count as zero. */\nexport function readAiTokenTotals(raw: unknown): AiTokenTotals {\n const record = isRecord(raw) ? raw : {}\n return {\n input: count(record['input']),\n cached: count(record['cached']),\n cacheWrite: count(record['cacheWrite']),\n output: count(record['output']),\n }\n}\n\n/**\n * The share of prompt tokens served from the cache: reads over everything the\n * prompt was billed as — sent, read and written.\n *\n * Writes sit in the denominator on purpose. A prefix that expires and is\n * rewritten every other request is the expensive failure, since a write costs\n * more than a fresh input token; a rate over reads and fresh input alone\n * reads that month as healthy. `null` when there are no prompt tokens at all,\n * which is nothing to divide rather than a 0% hit rate.\n */\nexport function aiCacheHitRate(\n tokens: Pick<AiTokenTotals, 'input' | 'cached' | 'cacheWrite'>,\n): number | null {\n const prompt = tokens.input + tokens.cached + tokens.cacheWrite\n return prompt > 0 ? tokens.cached / prompt : null\n}\n\n/** One kind of request in an org's month. */\nexport interface AiKindMonth {\n /** An `AiUsageKind`: `assist`, a besigner mode, or a job kind. */\n kind: string\n /** Metered model requests; a docs answer or a cache hit is not one. */\n requests: number\n /** What the kind DREW, at billed rates — the credits behind it. */\n estCostUsd: number\n /**\n * What the kind COST US (AGL-3015). Equal to `estCostUsd` on a kind served\n * only by models billed at their provider's list, and below it wherever a\n * marked-up model served. A month written before the split answers the\n * billed figure here, which over-reads our bill rather than under-reads it.\n */\n providerCostUsd: number\n tokens: AiTokenTotals\n}\n\n/**\n * The month document's `kinds` map as rows, dearest first.\n *\n * Ordered on what each kind DREW, not on what it cost us: the reader asking\n * \"which kind is expensive\" is asking about the meter every other figure on\n * the card is denominated in, and the two orders differ only where a kind's\n * model mix differs from its neighbour's.\n */\nexport function readAiKindMonths(raw: unknown): AiKindMonth[] {\n if (!isRecord(raw)) return []\n const rows: AiKindMonth[] = []\n for (const [kind, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue\n const cost = Number(value['estCostUsd'] ?? 0)\n const billed = Number.isFinite(cost) && cost > 0 ? cost : 0\n rows.push({\n kind,\n requests: count(value['requests']),\n estCostUsd: billed,\n providerCostUsd: assistProviderCostUsd(\n billed,\n value[ASSIST_PROVIDER_COST_FIELD],\n ),\n tokens: readAiTokenTotals(value[AI_USAGE_TOKENS_FIELD]),\n })\n }\n return rows.sort(\n (a, b) => b.estCostUsd - a.estCostUsd || b.requests - a.requests || a.kind.localeCompare(b.kind),\n )\n}\n"],"names":["assistProviderCostUsd","ASSIST_PROVIDER_COST_FIELD","AI_USAGE_MONTH_KINDS_FIELD","AI_USAGE_TOKENS_FIELD","count","value","parsed","Number","isFinite","Math","floor","isRecord","Array","isArray","emptyAiTokenTotals","input","cached","cacheWrite","output","aiTokenTotalsFrom","usage","inputTokens","cacheReadTokens","cacheWriteTokens","outputTokens","addAiTokenTotals","a","b","readAiTokenTotals","raw","record","aiCacheHitRate","tokens","prompt","readAiKindMonths","rows","kind","Object","entries","cost","billed","push","requests","estCostUsd","providerCostUsd","sort","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,0BAA0B,QACrB,wCAAuC;AAG9C;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,uCAAuC,GACvC,OAAO,MAAMC,6BAA6B,QAAO;AAEjD,sEAAsE,GACtE,OAAO,MAAMC,wBAAwB,SAAQ;AAa7C,MAAMC,QAAQ,CAACC;IACb,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;IAC/B,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIG,KAAKC,KAAK,CAACJ,UAAU;AACtE;AAEA,MAAMK,WAAW,CAACN,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACO,MAAMC,OAAO,CAACR;AAEhE,OAAO,SAASS;IACd,OAAO;QAAEC,OAAO;QAAGC,QAAQ;QAAGC,YAAY;QAAGC,QAAQ;IAAE;AACzD;AAEA,8CAA8C,GAC9C,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAO;QACLL,OAAOX,MAAMgB,MAAMC,WAAW;QAC9BL,QAAQZ,MAAMgB,MAAME,eAAe;QACnCL,YAAYb,MAAMgB,MAAMG,gBAAgB;QACxCL,QAAQd,MAAMgB,MAAMI,YAAY;IAClC;AACF;AAEA,OAAO,SAASC,iBAAiBC,CAAgB,EAAEC,CAAgB;IACjE,OAAO;QACLZ,OAAOW,EAAEX,KAAK,GAAGY,EAAEZ,KAAK;QACxBC,QAAQU,EAAEV,MAAM,GAAGW,EAAEX,MAAM;QAC3BC,YAAYS,EAAET,UAAU,GAAGU,EAAEV,UAAU;QACvCC,QAAQQ,EAAER,MAAM,GAAGS,EAAET,MAAM;IAC7B;AACF;AAEA,2EAA2E,GAC3E,OAAO,SAASU,kBAAkBC,GAAY;IAC5C,MAAMC,SAASnB,SAASkB,OAAOA,MAAM,CAAC;IACtC,OAAO;QACLd,OAAOX,MAAM0B,MAAM,CAAC,QAAQ;QAC5Bd,QAAQZ,MAAM0B,MAAM,CAAC,SAAS;QAC9Bb,YAAYb,MAAM0B,MAAM,CAAC,aAAa;QACtCZ,QAAQd,MAAM0B,MAAM,CAAC,SAAS;IAChC;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,eACdC,MAA8D;IAE9D,MAAMC,SAASD,OAAOjB,KAAK,GAAGiB,OAAOhB,MAAM,GAAGgB,OAAOf,UAAU;IAC/D,OAAOgB,SAAS,IAAID,OAAOhB,MAAM,GAAGiB,SAAS;AAC/C;AAoBA;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBL,GAAY;IAC3C,IAAI,CAAClB,SAASkB,MAAM,OAAO,EAAE;IAC7B,MAAMM,OAAsB,EAAE;IAC9B,KAAK,MAAM,CAACC,MAAM/B,MAAM,IAAIgC,OAAOC,OAAO,CAACT,KAAM;YAE3BxB;QADpB,IAAI,CAACM,SAASN,QAAQ;QACtB,MAAMkC,OAAOhC,QAAOF,oBAAAA,KAAK,CAAC,aAAa,YAAnBA,oBAAuB;QAC3C,MAAMmC,SAASjC,OAAOC,QAAQ,CAAC+B,SAASA,OAAO,IAAIA,OAAO;QAC1DJ,KAAKM,IAAI,CAAC;YACRL;YACAM,UAAUtC,MAAMC,KAAK,CAAC,WAAW;YACjCsC,YAAYH;YACZI,iBAAiB5C,sBACfwC,QACAnC,KAAK,CAACJ,2BAA2B;YAEnC+B,QAAQJ,kBAAkBvB,KAAK,CAACF,sBAAsB;QACxD;IACF;IACA,OAAOgC,KAAKU,IAAI,CACd,CAACnB,GAAGC,IAAMA,EAAEgB,UAAU,GAAGjB,EAAEiB,UAAU,IAAIhB,EAAEe,QAAQ,GAAGhB,EAAEgB,QAAQ,IAAIhB,EAAEU,IAAI,CAACU,aAAa,CAACnB,EAAES,IAAI;AAEnG"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-tokens.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 assistProviderCostUsd,\n ASSIST_PROVIDER_COST_FIELD,\n} from '../usage/assist-credits'\nimport type { AiUsage } from '../providers/contract'\n\n/**\n * TOKENS AS THE ROLLUPS KEEP THEM (AGL-2937): the pure half.\n *\n * A provider reports four counts per request, and each is priced at its own\n * rate: prompt tokens sent uncached, prompt tokens read from the prompt\n * cache, prompt tokens written to it, and tokens generated. The org month\n * (per kind), the person's month and the job step keep the same four, so a\n * figure read off one means what it means on the others, and the cache hit\n * rate is the same division everywhere it is shown.\n *\n * Stored shapes:\n *\n * orgs/{orgId}/assistUsage/{YYYY-MM}\n * kinds: { [kind]: { requests, estCostUsd, providerCostUsd,\n * tokens: { input, cached, cacheWrite, output } } }\n * orgs/{orgId}/aiUsageByUser/{uid}/months/{YYYY-MM}\n * tokens: { input, cached, cacheWrite, output }\n *\n * The month document's own top-level `inputTokens`, `outputTokens`,\n * `cacheReadTokens` and `cacheWriteTokens` stay its totals; `kinds` splits\n * them by what the request was for.\n */\n\n/** The month document's per-kind map. */\nexport const AI_USAGE_MONTH_KINDS_FIELD = 'kinds'\n\n/** The token map, on a person's month and on each kind of the org's. */\nexport const AI_USAGE_TOKENS_FIELD = 'tokens'\n\nexport interface AiTokenTotals {\n /** Prompt tokens sent uncached, at the model's input rate. */\n input: number\n /** Prompt tokens read from the prompt cache, at its read rate. */\n cached: number\n /** Prompt tokens written to the prompt cache, at its write rate. */\n cacheWrite: number\n /** Tokens the model generated, thinking included. */\n output: number\n}\n\nconst count = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nexport function emptyAiTokenTotals(): AiTokenTotals {\n return { input: 0, cached: 0, cacheWrite: 0, output: 0 }\n}\n\n/** A request's usage, in the rollups' names. */\nexport function aiTokenTotalsFrom(usage: AiUsage): AiTokenTotals {\n return {\n input: count(usage.inputTokens),\n cached: count(usage.cacheReadTokens),\n cacheWrite: count(usage.cacheWriteTokens),\n output: count(usage.outputTokens),\n }\n}\n\nexport function addAiTokenTotals(a: AiTokenTotals, b: AiTokenTotals): AiTokenTotals {\n return {\n input: a.input + b.input,\n cached: a.cached + b.cached,\n cacheWrite: a.cacheWrite + b.cacheWrite,\n output: a.output + b.output,\n }\n}\n\n/** A stored token map read back, every absent or malformed count as zero. */\nexport function readAiTokenTotals(raw: unknown): AiTokenTotals {\n const record = isRecord(raw) ? raw : {}\n return {\n input: count(record['input']),\n cached: count(record['cached']),\n cacheWrite: count(record['cacheWrite']),\n output: count(record['output']),\n }\n}\n\n/**\n * The share of prompt tokens served from the cache: reads over everything the\n * prompt was billed as — sent, read and written.\n *\n * Writes sit in the denominator on purpose. A prefix that expires and is\n * rewritten every other request is the expensive failure, since a write costs\n * more than a fresh input token; a rate over reads and fresh input alone\n * reads that month as healthy. `null` when there are no prompt tokens at all,\n * which is nothing to divide rather than a 0% hit rate.\n */\nexport function aiCacheHitRate(\n tokens: Pick<AiTokenTotals, 'input' | 'cached' | 'cacheWrite'>,\n): number | null {\n const prompt = tokens.input + tokens.cached + tokens.cacheWrite\n return prompt > 0 ? tokens.cached / prompt : null\n}\n\n/** One kind of request in an org's month. */\nexport interface AiKindMonth {\n /** An `AiUsageKind`: `assist`, a besigner mode, or a job kind. */\n kind: string\n /** Metered model requests; a docs answer or a cache hit is not one. */\n requests: number\n /** What the kind DREW, at billed rates — the credits behind it. */\n estCostUsd: number\n /**\n * What the kind COST US (AGL-3015). Equal to `estCostUsd` on a kind served\n * only by models billed at their provider's list, and below it wherever a\n * marked-up model served. A month written before the split answers the\n * billed figure here, which over-reads our bill rather than under-reads it.\n */\n providerCostUsd: number\n tokens: AiTokenTotals\n}\n\n/**\n * The month document's `kinds` map as rows, dearest first.\n *\n * Ordered on what each kind DREW, not on what it cost us: the reader asking\n * \"which kind is expensive\" is asking about the meter every other figure on\n * the card is denominated in, and the two orders differ only where a kind's\n * model mix differs from its neighbour's.\n */\nexport function readAiKindMonths(raw: unknown): AiKindMonth[] {\n if (!isRecord(raw)) return []\n const rows: AiKindMonth[] = []\n for (const [kind, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue\n const cost = Number(value['estCostUsd'] ?? 0)\n const billed = Number.isFinite(cost) && cost > 0 ? cost : 0\n rows.push({\n kind,\n requests: count(value['requests']),\n estCostUsd: billed,\n providerCostUsd: assistProviderCostUsd(\n billed,\n value[ASSIST_PROVIDER_COST_FIELD],\n ),\n tokens: readAiTokenTotals(value[AI_USAGE_TOKENS_FIELD]),\n })\n }\n return rows.sort(\n (a, b) => b.estCostUsd - a.estCostUsd || b.requests - a.requests || a.kind.localeCompare(b.kind),\n )\n}\n"],"names":["assistProviderCostUsd","ASSIST_PROVIDER_COST_FIELD","AI_USAGE_MONTH_KINDS_FIELD","AI_USAGE_TOKENS_FIELD","count","value","parsed","Number","isFinite","Math","floor","isRecord","Array","isArray","emptyAiTokenTotals","input","cached","cacheWrite","output","aiTokenTotalsFrom","usage","inputTokens","cacheReadTokens","cacheWriteTokens","outputTokens","addAiTokenTotals","a","b","readAiTokenTotals","raw","record","aiCacheHitRate","tokens","prompt","readAiKindMonths","rows","kind","Object","entries","cost","billed","push","requests","estCostUsd","providerCostUsd","sort","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,0BAA0B,QACrB,6BAAyB;AAGhC;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,uCAAuC,GACvC,OAAO,MAAMC,6BAA6B,QAAO;AAEjD,sEAAsE,GACtE,OAAO,MAAMC,wBAAwB,SAAQ;AAa7C,MAAMC,QAAQ,CAACC;IACb,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;IAC/B,OAAOE,OAAOC,QAAQ,CAACF,WAAWA,SAAS,IAAIG,KAAKC,KAAK,CAACJ,UAAU;AACtE;AAEA,MAAMK,WAAW,CAACN,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACO,MAAMC,OAAO,CAACR;AAEhE,OAAO,SAASS;IACd,OAAO;QAAEC,OAAO;QAAGC,QAAQ;QAAGC,YAAY;QAAGC,QAAQ;IAAE;AACzD;AAEA,8CAA8C,GAC9C,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAO;QACLL,OAAOX,MAAMgB,MAAMC,WAAW;QAC9BL,QAAQZ,MAAMgB,MAAME,eAAe;QACnCL,YAAYb,MAAMgB,MAAMG,gBAAgB;QACxCL,QAAQd,MAAMgB,MAAMI,YAAY;IAClC;AACF;AAEA,OAAO,SAASC,iBAAiBC,CAAgB,EAAEC,CAAgB;IACjE,OAAO;QACLZ,OAAOW,EAAEX,KAAK,GAAGY,EAAEZ,KAAK;QACxBC,QAAQU,EAAEV,MAAM,GAAGW,EAAEX,MAAM;QAC3BC,YAAYS,EAAET,UAAU,GAAGU,EAAEV,UAAU;QACvCC,QAAQQ,EAAER,MAAM,GAAGS,EAAET,MAAM;IAC7B;AACF;AAEA,2EAA2E,GAC3E,OAAO,SAASU,kBAAkBC,GAAY;IAC5C,MAAMC,SAASnB,SAASkB,OAAOA,MAAM,CAAC;IACtC,OAAO;QACLd,OAAOX,MAAM0B,MAAM,CAAC,QAAQ;QAC5Bd,QAAQZ,MAAM0B,MAAM,CAAC,SAAS;QAC9Bb,YAAYb,MAAM0B,MAAM,CAAC,aAAa;QACtCZ,QAAQd,MAAM0B,MAAM,CAAC,SAAS;IAChC;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,eACdC,MAA8D;IAE9D,MAAMC,SAASD,OAAOjB,KAAK,GAAGiB,OAAOhB,MAAM,GAAGgB,OAAOf,UAAU;IAC/D,OAAOgB,SAAS,IAAID,OAAOhB,MAAM,GAAGiB,SAAS;AAC/C;AAoBA;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBL,GAAY;IAC3C,IAAI,CAAClB,SAASkB,MAAM,OAAO,EAAE;IAC7B,MAAMM,OAAsB,EAAE;IAC9B,KAAK,MAAM,CAACC,MAAM/B,MAAM,IAAIgC,OAAOC,OAAO,CAACT,KAAM;YAE3BxB;QADpB,IAAI,CAACM,SAASN,QAAQ;QACtB,MAAMkC,OAAOhC,QAAOF,oBAAAA,KAAK,CAAC,aAAa,YAAnBA,oBAAuB;QAC3C,MAAMmC,SAASjC,OAAOC,QAAQ,CAAC+B,SAASA,OAAO,IAAIA,OAAO;QAC1DJ,KAAKM,IAAI,CAAC;YACRL;YACAM,UAAUtC,MAAMC,KAAK,CAAC,WAAW;YACjCsC,YAAYH;YACZI,iBAAiB5C,sBACfwC,QACAnC,KAAK,CAACJ,2BAA2B;YAEnC+B,QAAQJ,kBAAkBvB,KAAK,CAACF,sBAAsB;QACxD;IACF;IACA,OAAOgC,KAAKU,IAAI,CACd,CAACnB,GAAGC,IAAMA,EAAEgB,UAAU,GAAGjB,EAAEiB,UAAU,IAAIhB,EAAEe,QAAQ,GAAGhB,EAAEgB,QAAQ,IAAIhB,EAAEU,IAAI,CAACU,aAAa,CAACnB,EAAES,IAAI;AAEnG"}
@@ -13,7 +13,7 @@
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
- */ import { assistCreditsFromUsd } from "@aglyn/aglyn/app-utils/assist-credits";
16
+ */ import { assistCreditsFromUsd } from "../usage/assist-credits.js";
17
17
  /**
18
18
  * PER-USER AI USAGE (AGL-2928): the pure half.
19
19
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-usage-by-user.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 { AiJobKind } from './ai-jobs.types'\nimport { assistCreditsFromUsd } from '@aglyn/aglyn/app-utils/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the pure half.\n *\n * The org rollup at `assistUsage/{month}` says what a workspace spent on AI\n * and nothing about who spent it. This module is the shape and the arithmetic\n * of the second rollup — one document per person per month, under the org,\n * keyed by uid — so an agency or a multi-brand workspace can see which of its\n * people, on which site, is generating what. No prose, no PII beyond the uid\n * the document is keyed by.\n *\n * The write lives in the plugin's `usage/ai-usage-by-user.ts`, inside the\n * same batch as the org rollup. Everything here is dependency-free so the\n * plugin's tables and the doors that serve them read one definition of a\n * share, a month key and a retention window.\n */\n\n/** `orgs/{orgId}/{this}/{uid}/{months}/{YYYY-MM}` — the subcollection names. */\nexport const AI_USAGE_BY_USER_COLLECTION = 'aiUsageByUser'\nexport const AI_USAGE_BY_USER_MONTHS_COLLECTION = 'months'\n\n/**\n * How many months a person's usage is kept, counted from the month it\n * describes. Thirteen: a full year of month-over-month comparison for a\n * workspace admin, plus the month in progress. A TTL policy on the month\n * document's `expiresAt` enforces it — see `aiUsageByUserExpiry`.\n */\nexport const AI_USAGE_BY_USER_RETENTION_MONTHS = 13\n\n/**\n * What an AI request was FOR, as the per-user rollup buckets it.\n *\n * `assist` is a console chat turn; `element`, `blog` and `section` are the\n * besigner's three assist modes; the rest are generation job kinds, named\n * exactly as the job document names them so a reader can join the two — or\n * a purpose another plugin named on the text-generation seam.\n */\nexport type AiUsageKind =\n | 'assist'\n | 'element'\n | 'blog'\n | 'section'\n | AiJobKind\n | PluginAiUsageKind\n\n/**\n * A purpose another plugin named on the core's text-generation seam\n * (AGL-3324): the plugin's id and a word, hyphenated — `outreach-curate` —\n * so its spend sits in the same `byKind` map as a job kind and a reader can\n * tell whose it was.\n */\nexport type PluginAiUsageKind = `${string}-${string}`\n\n/** One person's month, zero-filled — the shape every reader hands on. */\nexport interface AiUsageByUserMonth {\n uid: string\n month: string\n /** Credits drawn, each request rounded up as the meter rounds. */\n credits: number\n /**\n * The measured provider spend behind `credits`. Server-side only: no\n * customer surface renders it, and `aiUsageShare` is how it becomes a\n * figure a customer may see.\n */\n estCostUsd: number\n requests: number\n refusals: number\n /** Credits by `AiUsageKind`. */\n byKind: Record<string, number>\n /** Credits by host id, for requests that named a site. */\n byHost: Record<string, number>\n}\n\nconst MONTH_KEY = /^\\d{4}-(0[1-9]|1[0-2])$/\n\n/** Whether `value` is a `YYYY-MM` month key. */\nexport function isAiUsageMonthKey(value: unknown): value is string {\n return typeof value === 'string' && MONTH_KEY.test(value)\n}\n\n/**\n * The month keys a reader may ask for, newest first: the month in progress\n * and the twelve before it — the retention window, so a picker built from\n * this list never names a month the TTL has reaped.\n */\nexport function aiUsageMonthKeys(\n now: Date = new Date(),\n count: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n): string[] {\n const keys: string[] = []\n for (let back = 0; back < count; back += 1) {\n keys.push(\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - back, 1))\n .toISOString()\n .slice(0, 7),\n )\n }\n return keys\n}\n\n/** Whether `month` is inside the window `aiUsageMonthKeys` describes. */\nexport function aiUsageMonthWithinRetention(\n month: string,\n now: Date = new Date(),\n): boolean {\n return isAiUsageMonthKey(month) && aiUsageMonthKeys(now).includes(month)\n}\n\n/**\n * When a month document expires: the first instant of the month that is\n * `AI_USAGE_BY_USER_RETENTION_MONTHS` after the one it describes has closed.\n *\n * A `Date`, not a number: the TTL policy keys on a Firestore Timestamp and\n * silently governs nothing when the field is a number. Deterministic per\n * month, so every increment into the document re-stamps the same value and\n * the policy never moves under a month that is still being written.\n */\nexport function aiUsageByUserExpiry(month: string): Date {\n if (!isAiUsageMonthKey(month)) {\n throw new Error(`not a month key: ${month}`)\n }\n const [year, monthOfYear] = month.split('-').map(Number)\n return new Date(\n Date.UTC(year, monthOfYear + AI_USAGE_BY_USER_RETENTION_MONTHS, 1),\n )\n}\n\n/**\n * The kind an assist request was, read off the route it arrived on — for the\n * meters that carry a route and no kind of their own. The three besigner\n * modes name themselves in the path; everything else is a chat turn.\n */\nexport function aiUsageKindFromRoute(route: string | null | undefined): AiUsageKind {\n const tail = String(route ?? '').split('/').filter(Boolean).pop() ?? ''\n if (tail === 'element' || tail === 'blog' || tail === 'section') return tail\n return 'assist'\n}\n\n/**\n * One person's share of the workspace's spend for the month, in `[0, 1]`.\n *\n * Measured on the dollar figures, never on credits: the org meter rounds the\n * month's spend up ONCE, while a person's credits round each request up, so\n * a roster's credits can sum to a little more than the pool they drew from.\n * The ratio of measured spends has no such drift. Clamped, because a month\n * document written a moment after the org rollup is still a fraction, not a\n * percentage over one hundred — and rounded to a basis point, which is finer\n * than any surface renders and coarser than floating-point noise.\n */\nexport function aiUsageShare(\n userEstCostUsd: number,\n orgEstCostUsd: number,\n): number {\n const user = Number(userEstCostUsd)\n const org = Number(orgEstCostUsd)\n if (!Number.isFinite(user) || user <= 0) return 0\n if (!Number.isFinite(org) || org <= 0) return 0\n return Math.min(1, Math.round((user / org) * 10_000) / 10_000)\n}\n\nconst finite = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0\n}\n\nconst creditMap = (raw: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n for (const [key, value] of Object.entries(\n (raw ?? {}) as Record<string, unknown>,\n )) {\n const credits = Math.floor(finite(value))\n if (credits > 0) out[key] = credits\n }\n return out\n}\n\n/**\n * A month document as a complete record. Absent and malformed fields read as\n * zero; a `credits` the writer never set is derived from the spend, so a\n * document written before the field existed still ranks.\n */\nexport function aiUsageByUserMonthFrom(\n raw: Record<string, unknown> | null | undefined,\n uid: string,\n month: string,\n): AiUsageByUserMonth {\n const estCostUsd = finite(raw?.['estCostUsd'])\n const stored = Math.floor(finite(raw?.['credits']))\n return {\n uid,\n month,\n credits: stored > 0 ? stored : assistCreditsFromUsd(estCostUsd),\n estCostUsd,\n requests: Math.floor(finite(raw?.['requests'])),\n refusals: Math.floor(finite(raw?.['refusals'])),\n byKind: creditMap(raw?.['byKind']),\n byHost: creditMap(raw?.['byHost']),\n }\n}\n\n/** The kinds a person drew the most credits on, dearest first. */\nexport function topAiUsageKinds(\n byKind: Record<string, number>,\n limit = 3,\n): Array<{ kind: string; credits: number }> {\n return Object.entries(byKind)\n .map(([kind, credits]) => ({ kind, credits }))\n .sort((a, b) => b.credits - a.credits || a.kind.localeCompare(b.kind))\n .slice(0, limit)\n}\n\n/** Two month documents for one person, summed — the roster column's figure. */\nexport function sumAiUsageCredits(months: readonly AiUsageByUserMonth[]): number {\n return months.reduce((sum, entry) => sum + entry.credits, 0)\n}\n"],"names":["assistCreditsFromUsd","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","MONTH_KEY","isAiUsageMonthKey","value","test","aiUsageMonthKeys","now","Date","count","keys","back","push","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiUsageMonthWithinRetention","month","includes","aiUsageByUserExpiry","Error","year","monthOfYear","split","map","Number","aiUsageKindFromRoute","route","String","tail","filter","Boolean","pop","aiUsageShare","userEstCostUsd","orgEstCostUsd","user","org","isFinite","Math","min","round","finite","parsed","creditMap","raw","out","key","Object","entries","credits","floor","aiUsageByUserMonthFrom","uid","estCostUsd","stored","requests","refusals","byKind","byHost","topAiUsageKinds","limit","kind","sort","a","b","localeCompare","sumAiUsageCredits","months","reduce","sum","entry"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-usage-by-user.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 { AiJobKind } from './ai-jobs.types'\nimport { assistCreditsFromUsd } from '../usage/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the pure half.\n *\n * The org rollup at `assistUsage/{month}` says what a workspace spent on AI\n * and nothing about who spent it. This module is the shape and the arithmetic\n * of the second rollup — one document per person per month, under the org,\n * keyed by uid — so an agency or a multi-brand workspace can see which of its\n * people, on which site, is generating what. No prose, no PII beyond the uid\n * the document is keyed by.\n *\n * The write lives in the plugin's `usage/ai-usage-by-user.ts`, inside the\n * same batch as the org rollup. Everything here is dependency-free so the\n * plugin's tables and the doors that serve them read one definition of a\n * share, a month key and a retention window.\n */\n\n/** `orgs/{orgId}/{this}/{uid}/{months}/{YYYY-MM}` — the subcollection names. */\nexport const AI_USAGE_BY_USER_COLLECTION = 'aiUsageByUser'\nexport const AI_USAGE_BY_USER_MONTHS_COLLECTION = 'months'\n\n/**\n * How many months a person's usage is kept, counted from the month it\n * describes. Thirteen: a full year of month-over-month comparison for a\n * workspace admin, plus the month in progress. A TTL policy on the month\n * document's `expiresAt` enforces it — see `aiUsageByUserExpiry`.\n */\nexport const AI_USAGE_BY_USER_RETENTION_MONTHS = 13\n\n/**\n * What an AI request was FOR, as the per-user rollup buckets it.\n *\n * `assist` is a console chat turn; `element`, `blog` and `section` are the\n * besigner's three assist modes; the rest are generation job kinds, named\n * exactly as the job document names them so a reader can join the two — or\n * a purpose another plugin named on the text-generation seam.\n */\nexport type AiUsageKind =\n | 'assist'\n | 'element'\n | 'blog'\n | 'section'\n | AiJobKind\n | PluginAiUsageKind\n\n/**\n * A purpose another plugin named on the core's text-generation seam\n * (AGL-3324): the plugin's id and a word, hyphenated — `outreach-curate` —\n * so its spend sits in the same `byKind` map as a job kind and a reader can\n * tell whose it was.\n */\nexport type PluginAiUsageKind = `${string}-${string}`\n\n/** One person's month, zero-filled — the shape every reader hands on. */\nexport interface AiUsageByUserMonth {\n uid: string\n month: string\n /** Credits drawn, each request rounded up as the meter rounds. */\n credits: number\n /**\n * The measured provider spend behind `credits`. Server-side only: no\n * customer surface renders it, and `aiUsageShare` is how it becomes a\n * figure a customer may see.\n */\n estCostUsd: number\n requests: number\n refusals: number\n /** Credits by `AiUsageKind`. */\n byKind: Record<string, number>\n /** Credits by host id, for requests that named a site. */\n byHost: Record<string, number>\n}\n\nconst MONTH_KEY = /^\\d{4}-(0[1-9]|1[0-2])$/\n\n/** Whether `value` is a `YYYY-MM` month key. */\nexport function isAiUsageMonthKey(value: unknown): value is string {\n return typeof value === 'string' && MONTH_KEY.test(value)\n}\n\n/**\n * The month keys a reader may ask for, newest first: the month in progress\n * and the twelve before it — the retention window, so a picker built from\n * this list never names a month the TTL has reaped.\n */\nexport function aiUsageMonthKeys(\n now: Date = new Date(),\n count: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n): string[] {\n const keys: string[] = []\n for (let back = 0; back < count; back += 1) {\n keys.push(\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - back, 1))\n .toISOString()\n .slice(0, 7),\n )\n }\n return keys\n}\n\n/** Whether `month` is inside the window `aiUsageMonthKeys` describes. */\nexport function aiUsageMonthWithinRetention(\n month: string,\n now: Date = new Date(),\n): boolean {\n return isAiUsageMonthKey(month) && aiUsageMonthKeys(now).includes(month)\n}\n\n/**\n * When a month document expires: the first instant of the month that is\n * `AI_USAGE_BY_USER_RETENTION_MONTHS` after the one it describes has closed.\n *\n * A `Date`, not a number: the TTL policy keys on a Firestore Timestamp and\n * silently governs nothing when the field is a number. Deterministic per\n * month, so every increment into the document re-stamps the same value and\n * the policy never moves under a month that is still being written.\n */\nexport function aiUsageByUserExpiry(month: string): Date {\n if (!isAiUsageMonthKey(month)) {\n throw new Error(`not a month key: ${month}`)\n }\n const [year, monthOfYear] = month.split('-').map(Number)\n return new Date(\n Date.UTC(year, monthOfYear + AI_USAGE_BY_USER_RETENTION_MONTHS, 1),\n )\n}\n\n/**\n * The kind an assist request was, read off the route it arrived on — for the\n * meters that carry a route and no kind of their own. The three besigner\n * modes name themselves in the path; everything else is a chat turn.\n */\nexport function aiUsageKindFromRoute(route: string | null | undefined): AiUsageKind {\n const tail = String(route ?? '').split('/').filter(Boolean).pop() ?? ''\n if (tail === 'element' || tail === 'blog' || tail === 'section') return tail\n return 'assist'\n}\n\n/**\n * One person's share of the workspace's spend for the month, in `[0, 1]`.\n *\n * Measured on the dollar figures, never on credits: the org meter rounds the\n * month's spend up ONCE, while a person's credits round each request up, so\n * a roster's credits can sum to a little more than the pool they drew from.\n * The ratio of measured spends has no such drift. Clamped, because a month\n * document written a moment after the org rollup is still a fraction, not a\n * percentage over one hundred — and rounded to a basis point, which is finer\n * than any surface renders and coarser than floating-point noise.\n */\nexport function aiUsageShare(\n userEstCostUsd: number,\n orgEstCostUsd: number,\n): number {\n const user = Number(userEstCostUsd)\n const org = Number(orgEstCostUsd)\n if (!Number.isFinite(user) || user <= 0) return 0\n if (!Number.isFinite(org) || org <= 0) return 0\n return Math.min(1, Math.round((user / org) * 10_000) / 10_000)\n}\n\nconst finite = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0\n}\n\nconst creditMap = (raw: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n for (const [key, value] of Object.entries(\n (raw ?? {}) as Record<string, unknown>,\n )) {\n const credits = Math.floor(finite(value))\n if (credits > 0) out[key] = credits\n }\n return out\n}\n\n/**\n * A month document as a complete record. Absent and malformed fields read as\n * zero; a `credits` the writer never set is derived from the spend, so a\n * document written before the field existed still ranks.\n */\nexport function aiUsageByUserMonthFrom(\n raw: Record<string, unknown> | null | undefined,\n uid: string,\n month: string,\n): AiUsageByUserMonth {\n const estCostUsd = finite(raw?.['estCostUsd'])\n const stored = Math.floor(finite(raw?.['credits']))\n return {\n uid,\n month,\n credits: stored > 0 ? stored : assistCreditsFromUsd(estCostUsd),\n estCostUsd,\n requests: Math.floor(finite(raw?.['requests'])),\n refusals: Math.floor(finite(raw?.['refusals'])),\n byKind: creditMap(raw?.['byKind']),\n byHost: creditMap(raw?.['byHost']),\n }\n}\n\n/** The kinds a person drew the most credits on, dearest first. */\nexport function topAiUsageKinds(\n byKind: Record<string, number>,\n limit = 3,\n): Array<{ kind: string; credits: number }> {\n return Object.entries(byKind)\n .map(([kind, credits]) => ({ kind, credits }))\n .sort((a, b) => b.credits - a.credits || a.kind.localeCompare(b.kind))\n .slice(0, limit)\n}\n\n/** Two month documents for one person, summed — the roster column's figure. */\nexport function sumAiUsageCredits(months: readonly AiUsageByUserMonth[]): number {\n return months.reduce((sum, entry) => sum + entry.credits, 0)\n}\n"],"names":["assistCreditsFromUsd","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","MONTH_KEY","isAiUsageMonthKey","value","test","aiUsageMonthKeys","now","Date","count","keys","back","push","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiUsageMonthWithinRetention","month","includes","aiUsageByUserExpiry","Error","year","monthOfYear","split","map","Number","aiUsageKindFromRoute","route","String","tail","filter","Boolean","pop","aiUsageShare","userEstCostUsd","orgEstCostUsd","user","org","isFinite","Math","min","round","finite","parsed","creditMap","raw","out","key","Object","entries","credits","floor","aiUsageByUserMonthFrom","uid","estCostUsd","stored","requests","refusals","byKind","byHost","topAiUsageKinds","limit","kind","sort","a","b","localeCompare","sumAiUsageCredits","months","reduce","sum","entry"],"mappings":"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"}
@@ -58,7 +58,7 @@ const OPENAI_COMPATIBLE = 'openai-compatible';
58
58
  purpose: "AI-assisted features in the console and the site editor: the Aglyn Assist helper, including changes it proposes to a page, component, or layout open in the editor; editor assistance (rewriting element copy, drafting blog bodies, generating a section layout); and AI generation, which creates drafts and proposals for a customer's site from a brief or from what the site already holds, such as copy, layouts, templates, automations, search titles and descriptions, theme changes, product descriptions and tags, draft products, and store categories and discounts; explanations of a site's automations, including why an automation's run failed; AI insights, which answer a customer's questions about its own figures, such as site traffic, sales, bookings, forms, campaigns, A/B tests and datasets, and write a weekly summary of them for members who ask for one; and AI assistance in the CRM, which summarizes a contact, company, deal or lead for a user and suggests a next step, drafts a one-to-one email for the user to review and send, and suggests how the columns of a spreadsheet the user imports match CRM fields",
59
59
  publishedOn: '2026-09-17',
60
60
  reason: "Reached through the AI plugin's Anthropic adapter (`libs/plugins/ai/src/lib/providers/anthropic.ts`) by the doors that call the AI runtime. `libs/plugins/ai/src/lib/server/assist-chat.ts` is gated by `release_assist` AND the key, and a generation job's text step by `release_ai_generative`; `libs/plugins/ai/src/lib/server/ai-assist.ts` carries NO release flag, so setting `{apiKeyEnv}` in production is by itself what starts this flow. `assist-anthropic-subprocessor-gate.spec.ts` holds the per-door detail and is the deeper guard for this one vendor.",
61
- dataReceived: "What the user submits — a question, instruction or brief, with the earlier messages of the same Assist conversation — and the content of the element, post, section or page being worked on, with the generated response. On Pro and above, the organization's name and the console route and host travel with an Assist question. For an edit the assistant proposes in the besigner, an outline of the open page, component or layout: element and component ids, layer names, shortened setting values and the selected element's styles. For a generation job, the site inventory: the names and addresses of its screens and collections, the names of its components, layouts, templates, forms and datasets with their prop and field names, and the theme's summary, colors and fonts. For a theme change, the site's current theme settings and brand colors as hex values, from the organization's brand settings, the site logo in the media library or a public page the brief links to. For features that review or write search information, the text and structure of the pages concerned. For product copy, the store's name, the product's name, type, description, tags, options and search listing, the store's category names, and the product's first media-library photo as a copy at most 768 px on its longer edge with its metadata stripped; never another media file, a price, stock, an order or a customer. For products, categories and discounts proposed from a brief, the store's name and its existing category names. For an automation drafted from a brief, which of CRM, webhooks and bookings the plan includes; for an explanation, the automation's outline (trigger, conditions, each step's text with the names of the lists, campaigns, workflows, webhooks and datasets it uses and whether each exists, and a workflow's function names and expressions) and, for a failed run, its time, steps and recorded errors, with email addresses removed and never the triggering event's data. For an insight, the figure reports available and aggregate tables: traffic with top page paths, referrers and campaign tags; form views and submissions; revenue, orders and best-selling product names; bookings by service; campaign subjects with delivery, open and click rates; A/B test and variant conversions; and, for a dataset the member can see, field names and types, record and fill counts, number ranges and totals grouped by a value at least three records share. Email addresses and phone numbers are removed, and no individual record is sent. For CRM assistance, the opened contact, company, deal or lead as the CRM shows it: its name and, by kind, job title, company, lifecycle stage, tags, capture history and counts, domain, industry, headcount, pipeline stages, status, amount, dates, lost reason, parties and lead status, with its notes, newest timeline entries and open tasks and deals. Email addresses and phone numbers in that text are replaced, and no email, phone or postal field, marketing consent, custom field value, team member or record id is sent. An email draft adds the request and the record's merge field names; an import sends the field names and types and each column's header and value kind, never a row. No account identifiers, email addresses or authentication tokens."
61
+ dataReceived: "What the user submits — a question, instruction or brief, with the earlier messages of the same Assist conversation — and the content of the element, post, section or page being worked on, with the generated response. On Pro and above, the organization's name and the console route and host travel with an Assist question. For an edit the assistant proposes in the besigner, an outline of the open page, component or layout: element and component ids, layer names, shortened setting values and the selected element's styles. For a generation job, the site inventory: the names and addresses of its pages and collections, the names of its components, layouts, templates, forms and datasets with their prop and field names, and the theme's summary, colors and fonts. For a theme change, the site's current theme settings and brand colors as hex values, from the organization's brand settings, the site logo in the media library or a public page the brief links to. For features that review or write search information, the text and structure of the pages concerned. For product copy, the store's name, the product's name, type, description, tags, options and search listing, the store's category names, and the product's first media-library photo as a copy at most 768 px on its longer edge with its metadata stripped; never another media file, a price, stock, an order or a customer. For products, categories and discounts proposed from a brief, the store's name and its existing category names. For an automation drafted from a brief, which of CRM, webhooks and bookings the plan includes; for an explanation, the automation's outline (trigger, conditions, each step's text with the names of the lists, campaigns, workflows, webhooks and datasets it uses and whether each exists, and a workflow's function names and expressions) and, for a failed run, its time, steps and recorded errors, with email addresses removed and never the triggering event's data. For an insight, the figure reports available and aggregate tables: traffic with top page paths, referrers and campaign tags; form views and submissions; revenue, orders and best-selling product names; bookings by service; campaign subjects with delivery, open and click rates; A/B test and variant conversions; and, for a dataset the member can see, field names and types, record and fill counts, number ranges and totals grouped by a value at least three records share. Email addresses and phone numbers are removed, and no individual record is sent. For CRM assistance, the opened contact, company, deal or lead as the CRM shows it: its name and, by kind, job title, company, lifecycle stage, tags, capture history and counts, domain, industry, headcount, pipeline stages, status, amount, dates, lost reason, parties and lead status, with its notes, newest timeline entries and open tasks and deals. Email addresses and phone numbers in that text are replaced, and no email, phone or postal field, marketing consent, custom field value, team member or record id is sent. An email draft adds the request and the record's merge field names; an import sends the field names and types and each column's header and value kind, never a row. No account identifiers, email addresses or authentication tokens."
62
62
  }
63
63
  },
64
64
  {