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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +7 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +21 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +98 -23
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  16. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  17. package/src/lib/declarations.server.js +12 -0
  18. package/src/lib/declarations.server.js.map +1 -1
  19. package/src/lib/jobs/ai-build-capabilities.js +216 -2
  20. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  21. package/src/lib/jobs/ai-job-build-step.d.ts +3 -1
  22. package/src/lib/jobs/ai-job-build-step.js +124 -18
  23. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  25. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  26. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-form-dataset.d.ts +8 -5
  28. package/src/lib/jobs/ai-job-form-dataset.js +28 -10
  29. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
  30. package/src/lib/jobs/ai-job-form-step.d.ts +20 -4
  31. package/src/lib/jobs/ai-job-form-step.js +98 -28
  32. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-layout-language.d.ts +6 -2
  34. package/src/lib/jobs/ai-job-layout-language.js +29 -5
  35. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  36. package/src/lib/jobs/ai-job-layout-step.js +4 -1
  37. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  38. package/src/lib/jobs/ai-job-media-step.d.ts +239 -0
  39. package/src/lib/jobs/ai-job-media-step.js +610 -0
  40. package/src/lib/jobs/ai-job-media-step.js.map +1 -0
  41. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  42. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  43. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  44. package/src/lib/jobs/ai-job-page-language.d.ts +4 -1
  45. package/src/lib/jobs/ai-job-page-language.js +30 -5
  46. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-page-sections.js +5 -4
  48. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-step.js +3 -2
  50. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  51. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -3
  52. package/src/lib/jobs/ai-job-plan-step.js +87 -20
  53. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  55. package/src/lib/jobs/ai-job-products-step.js +2 -2
  56. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-settings-step.d.ts +268 -0
  58. package/src/lib/jobs/ai-job-settings-step.js +690 -0
  59. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  60. package/src/lib/jobs/ai-job-site-content.d.ts +22 -8
  61. package/src/lib/jobs/ai-job-site-content.js +33 -15
  62. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-datasets.d.ts +3 -1
  64. package/src/lib/jobs/ai-job-site-datasets.js +13 -5
  65. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  66. package/src/lib/jobs/ai-job-site-events.d.ts +148 -0
  67. package/src/lib/jobs/ai-job-site-events.js +313 -0
  68. package/src/lib/jobs/ai-job-site-events.js.map +1 -0
  69. package/src/lib/jobs/ai-job-site-step.d.ts +12 -4
  70. package/src/lib/jobs/ai-job-site-step.js +132 -26
  71. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-tracking-step.d.ts +184 -0
  73. package/src/lib/jobs/ai-job-tracking-step.js +501 -0
  74. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -0
  75. package/src/lib/jobs/ai-layout-stock-photos.d.ts +21 -0
  76. package/src/lib/jobs/ai-layout-stock-photos.js +41 -4
  77. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  78. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  79. package/src/lib/jobs/ai-site-publish.js +202 -56
  80. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  81. package/src/lib/jobs/ai-stock-photo-picker.d.ts +24 -0
  82. package/src/lib/jobs/ai-stock-photo-picker.js +108 -0
  83. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -0
  84. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.d.ts +37 -0
  85. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js +47 -0
  86. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js.map +1 -0
  87. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.d.ts +21 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js +128 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js.map +1 -0
  90. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -7
  91. package/src/lib/layout-language/ai-layout-compiler.js +62 -42
  92. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  93. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  94. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  95. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  96. package/src/lib/layout-language/ai-layout-frame.d.ts +24 -5
  97. package/src/lib/layout-language/ai-layout-frame.js +93 -20
  98. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  99. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  101. package/src/lib/layout-language/ai-layout-labels.d.ts +70 -0
  102. package/src/lib/layout-language/ai-layout-labels.js +162 -0
  103. package/src/lib/layout-language/ai-layout-labels.js.map +1 -0
  104. package/src/lib/layout-language/ai-layout-links.js +5 -2
  105. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  106. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  107. package/src/lib/layout-language/ai-layout-listings.js +66 -5
  108. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  109. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  110. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  111. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  112. package/src/lib/model/ai-build-event-page.d.ts +70 -0
  113. package/src/lib/model/ai-build-event-page.js +145 -0
  114. package/src/lib/model/ai-build-event-page.js.map +1 -0
  115. package/src/lib/model/ai-build-job.d.ts +44 -0
  116. package/src/lib/model/ai-build-job.js +141 -13
  117. package/src/lib/model/ai-build-job.js.map +1 -1
  118. package/src/lib/model/ai-build-plan.d.ts +39 -3
  119. package/src/lib/model/ai-build-plan.js +59 -5
  120. package/src/lib/model/ai-build-plan.js.map +1 -1
  121. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  122. package/src/lib/model/ai-credit-estimate.js +51 -0
  123. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  124. package/src/lib/model/ai-job-activity.js +6 -1
  125. package/src/lib/model/ai-job-activity.js.map +1 -1
  126. package/src/lib/model/ai-jobs.types.d.ts +18 -2
  127. package/src/lib/model/ai-jobs.types.js +6 -1
  128. package/src/lib/model/ai-jobs.types.js.map +1 -1
  129. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  130. package/src/lib/model/ai-layout-menus.js +518 -0
  131. package/src/lib/model/ai-layout-menus.js.map +1 -0
  132. package/src/lib/model/ai-plan-capabilities.d.ts +8 -0
  133. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  134. package/src/lib/model/ai-products.d.ts +31 -1
  135. package/src/lib/model/ai-products.js +34 -1
  136. package/src/lib/model/ai-products.js.map +1 -1
  137. package/src/lib/model/ai-site-inventory.d.ts +17 -0
  138. package/src/lib/model/ai-site-inventory.js +5 -0
  139. package/src/lib/model/ai-site-inventory.js.map +1 -1
  140. package/src/lib/model/ai-site-job.d.ts +12 -2
  141. package/src/lib/model/ai-site-job.js +39 -12
  142. package/src/lib/model/ai-site-job.js.map +1 -1
  143. package/src/lib/model/ai-site-store-pages.js +1 -1
  144. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  145. package/src/lib/runtime/ai-doctrine-validators.d.ts +78 -3
  146. package/src/lib/runtime/ai-doctrine-validators.js +219 -31
  147. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  148. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  149. package/src/lib/runtime/ai-doctrine.js +16 -7
  150. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  151. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  152. package/src/lib/runtime/ai-eval-live.js +1 -1
  153. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  154. package/src/lib/runtime/ai-palette.d.ts +3 -1
  155. package/src/lib/runtime/ai-palette.generated.js +499 -11
  156. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  157. package/src/lib/runtime/ai-palette.js +5 -3
  158. package/src/lib/runtime/ai-palette.js.map +1 -1
  159. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  160. package/src/lib/runtime/ai-products-generation.js +12 -6
  161. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  162. package/src/lib/runtime/ai-template-examples.js +8 -2
  163. package/src/lib/runtime/ai-template-examples.js.map +1 -1
  164. package/src/lib/runtime/site-inventory.d.ts +12 -0
  165. package/src/lib/runtime/site-inventory.js +29 -1
  166. package/src/lib/runtime/site-inventory.js.map +1 -1
  167. package/src/lib/server.js +20 -0
  168. package/src/lib/server.js.map +1 -1
  169. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  170. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  171. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  172. package/src/lib/tools/ai-products-tool.js +15 -5
  173. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -133,7 +133,9 @@ export const AI_OUTPUT_KINDS = [
133
133
  * A page is a few hundred elements and tens of kilobytes of stored map; a
134
134
  * reusable component is one repeated block; a layout is the chrome every
135
135
  * page loads, so it carries the least imagery; a form is fields and a
136
- * button; an email is held to the clipping threshold on its rendered HTML.
136
+ * button — each field a Form Control over a Form Label and its answer, a
137
+ * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen
138
+ * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.
137
139
  * No kind names a font beyond the theme's.
138
140
  *
139
141
  * A third-party player is a page's or a component's to carry, and only where
@@ -171,8 +173,8 @@ export const AI_OUTPUT_KINDS = [
171
173
  fontFamilies: 0
172
174
  },
173
175
  form: {
174
- nodes: 60,
175
- bytes: 8000,
176
+ nodes: 150,
177
+ bytes: 15000,
176
178
  imageBytes: 0,
177
179
  embeds: 0,
178
180
  fontFamilies: 0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 60,\n bytes: 8_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAgBD,OAAO,MAAMA,mBAAyC;IACpD;IACA;IACA;IACA;IACA;CACD,CAAA;AAmGD;;;CAGC,GACD,OAAO,SAASC,oBACdC,YAAwD,EACxDC,eAAwB;IAMxB,MAAMC,aAA2C,CAAC;IAClD,MAAMC,YAAwC,CAAC;IAC/C,MAAMC,aAAqC,CAAC;IAC5C,KAAK,MAAMC,cAAcC,OAAOC,MAAM,CAACP,cAAe;QACpD,KAAK,MAAM,CAACQ,MAAMC,OAAO,IAAIH,OAAOI,OAAO,CACzCL,WAAWM,WAAW,CAACT,UAAU,EAChC;YACD,IAAI,CAACD,mBAAmBI,WAAWO,iBAAiB,CAACC,QAAQ,CAACL,OAC5D;YACFN,UAAU,CAACM,KAAK,GAAGC;YACnB,MAAMK,OAAOT,WAAWF,SAAS,CAACK,KAAK;YACvC,IAAIM,MAAMX,SAAS,CAACK,KAAK,GAAGM;YAC5B,MAAMC,QAAQV,WAAWD,UAAU,CAACI,KAAK;YACzC,IAAIO,UAAUC,WAAWZ,UAAU,CAACI,KAAK,GAAGO;QAC9C;IACF;IACA,OAAO;QAAEb;QAAYC;QAAWC;IAAW;AAC7C;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASa,sBACdC,QAIE,EACFC,OAAmD,EACnDC,iBAAsC;IAEtC,MAAMC,WAAqB,EAAE;IAC7B,KAAK,MAAMhB,cAAca,SAAU;QACjC,MAAMI,QAAQH,OAAO,CAACd,WAAWkB,EAAE,CAAC;QACpC,IAAI,CAACD,OAAO;YACVD,SAASG,IAAI,CACX,CAAC,wCAAwC,EAAEnB,WAAWkB,EAAE,CAAC,+BAA+B,CAAC;YAE3F;QACF;QACA,IAAI,CAACjB,OAAOmB,IAAI,CAACH,MAAMX,WAAW,CAACT,UAAU,EAAEwB,MAAM,EAAE;YACrDL,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,2CAA2C,CAAC;QAElF;QACA,KAAK,MAAMI,aAAatB,WAAWuB,UAAU,CAAE;YAC7C,IAAIN,MAAMX,WAAW,CAACT,UAAU,CAACyB,UAAUnB,IAAI,CAAC,EAAE;YAClD,IAAI,CAACc,MAAMO,YAAY,CAAChB,QAAQ,CAACc,UAAUnB,IAAI,GAAG;gBAChDa,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,YAAY,EAAEI,UAAUnB,IAAI,CAAC,0DAA0D,CAAC;gBAE5H;YACF;YACA,IAAIY,kBAAkBU,GAAG,CAACH,UAAUI,SAAS,GAAG;gBAC9CV,SAASG,IAAI,CACX,CAAC,iBAAiB,EAAEnB,WAAWkB,EAAE,CAAC,SAAS,EAAEI,UAAUnB,IAAI,CAAC,UAAU,EAAEmB,UAAUI,SAAS,CAAC,oCAAoC,CAAC;YAErI;QACF;IACF;IACA,KAAK,MAAMR,MAAMjB,OAAOmB,IAAI,CAACN,SAAU;QACrC,IAAI,CAACD,SAASc,IAAI,CAAC,CAAC3B,aAAeA,WAAWkB,EAAE,KAAKA,KAAK;YACxDF,SAASG,IAAI,CACX,CAAC,qCAAqC,EAAED,GAAG,gCAAgC,CAAC;QAEhF;IACF;IACA,OAAOF;AACT;AA4BA;;;;CAIC,GACD,OAAO,MAAMY,iBAAiB;IAC5BC,UAAU;IACVC,MAAM;IACNC,QAAQ;IACR,qCAAqC,GACrCC,OAAO;AACT,EAAU;AAEV;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,KAAK;AAClD,OAAO,MAAMC,+BAA+BD,gCAAgC,EAAC;AAE7E,0DAA0D,GAC1D,OAAO,SAASE,sBAAsBC,OAAe;IACnD,OAAOC,KAAKC,IAAI,CAACF,QAAQf,MAAM,GAAG;AACpC;AAcA,OAAO,MAAMkB,kBAA2C;IACtD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,iDAAiD,GACjD,OAAO,MAAMC,oBAAqD;IAChEC,MAAM;IACNC,UAAU;IACVhB,WAAW;IACXiB,QAAQ;IACRC,MAAM;IACNC,OAAO;AACT,EAAC;AA2BD;;;CAGC,GACD,OAAO,MAAMC,sBAAsB,OAAO;AAE1C;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,oBAA0D;IACrEN,MAAM;QACJO,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAV,UAAU;QACRM,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACA1B,WAAW;QACTsB,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAT,QAAQ;QACNK,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAR,MAAM;QACJI,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;IAChB;IACAP,OAAO;QACLG,OAAO;QACPC,OAAO;QACPC,YAAY;QACZC,QAAQ;QACRC,cAAc;QACdC,gBAAgBP;IAClB;AACF,EAAC;AAqBD,4EAA4E,GAC5E,OAAO,SAASQ,kBAAkBC,OAAgB;IAChD,OAAO,OAAOA,YAAY,YAAY,gCAAgCC,IAAI,CAACD;AAC7E"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-palette.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 { JsonLinealOrder } from '@aglyn/aglyn/app-utils/lineal-order'\n\n/**\n * The element palette as a model sees it (AGL-2905).\n *\n * `ai-palette.generated.ts` is produced from the registered plugin bundles by\n * `libs/plugins/ai/scripts/generate-ai-palette.mts` and holds the data; this module\n * holds the shapes that data is typed against and the small readers over it,\n * so a regeneration never rewrites a type.\n */\n\n/** The document kinds a model is asked to compose. */\nexport type AiSurface = 'screen' | 'email' | 'form' | 'layout' | 'component'\n\nexport const AI_SURFACE_NAMES: readonly AiSurface[] = [\n 'screen',\n 'email',\n 'form',\n 'layout',\n 'component',\n]\n\n/**\n * The subset of JSON Schema a prop declaration is expressed in. Enough to\n * validate what an attribute field can persist; nothing the field editor\n * cannot itself model.\n */\nexport interface AiPropSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean'\n enum?: readonly string[]\n minLength?: number\n maxLength?: number\n minimum?: number\n maximum?: number\n pattern?: string\n description?: string\n}\n\nexport interface AiPropsSchema {\n type: 'object'\n properties: Record<string, AiPropSchema>\n required: readonly string[]\n additionalProperties: false\n}\n\n/**\n * What a string prop MEANS, which decides how the validator checks it.\n *\n * - `text` — copy the visitor reads; held to a length ceiling.\n * - `url` — a navigable address: an `https:` URL or a root-relative path.\n * - `screen` — a screen id, checked against the screens the host has.\n * - `media` — an image or video source: a DAM reference or an `https:` URL.\n */\nexport type AiPropRole = 'text' | 'url' | 'screen' | 'media'\n\n/**\n * A `restrictChildren` / `restrictParent` directive as generated data: the\n * same tuple the schema declares, with the flag spelled as the string the\n * enum holds so a JSON emitter can write it.\n */\nexport type AiLinealOrder = JsonLinealOrder\n\nexport interface AiPaletteEntry {\n pluginId: string\n kind: 'element' | 'plaintext' | 'markdown'\n category: string\n displayName: string\n /** One sentence, from the schema's own description. */\n summary: string\n /** Whether the editor lets anything be dropped inside this element. */\n acceptsChildren: boolean\n restrictChildren?: AiLinealOrder\n restrictParent?: AiLinealOrder\n propsSchema: AiPropsSchema\n /** Roles of the string props the validator treats specially. */\n propRoles: Record<string, AiPropRole>\n /**\n * The attribute field kind each declared prop is edited with, as its\n * `FieldComponentType` value (`switch`, `select`, `screen-select`, …): what\n * the editor reads to decide which component properties a field can be\n * bound to (AGL-2908).\n */\n propFields: Record<string, string>\n /** Length ceilings for `text` props, in characters. */\n textLimits: Record<string, number>\n /** Names of the presets that place this component. */\n presets: readonly string[]\n}\n\n/**\n * What every NODE can do, as against what one component declares (AGL-3156).\n *\n * Generated from the node capabilities core declares, beside the per-component\n * schemas rather than out of them: repeating is not a Stack's feature, so no\n * component schema names it and a palette built from schemas alone advertises\n * it on nothing. Declared ONCE for the whole palette — a model may write these\n * props on any element the surface allows.\n */\nexport interface AiNodeCapability {\n /** Stable id, as core declares it. */\n id: string\n displayName: string\n /** One sentence, for the prompt catalog. */\n summary: string\n propsSchema: AiPropsSchema\n propRoles: Record<string, AiPropRole>\n propFields: Record<string, string>\n textLimits: Record<string, number>\n /** Props offered only on a node that holds a child list. */\n childrenOnlyProps: readonly string[]\n /**\n * Declared attributes the palette deliberately does not offer: a picker\n * that chooses a record the site holds, which a model cannot name from a\n * description. Recorded so a field left off on purpose reads differently\n * from one that went missing.\n */\n omittedProps: readonly string[]\n}\n\n/**\n * The capability props a node with this child contract may carry, merged into\n * one map the prop sanitizer can read beside the element's own declarations.\n */\nexport function nodeCapabilityProps(\n capabilities: Readonly<Record<string, AiNodeCapability>>,\n acceptsChildren: boolean,\n): {\n properties: Record<string, AiPropSchema>\n propRoles: Record<string, AiPropRole>\n textLimits: Record<string, number>\n} {\n const properties: Record<string, AiPropSchema> = {}\n const propRoles: Record<string, AiPropRole> = {}\n const textLimits: Record<string, number> = {}\n for (const capability of Object.values(capabilities)) {\n for (const [name, schema] of Object.entries(\n capability.propsSchema.properties,\n )) {\n if (!acceptsChildren && capability.childrenOnlyProps.includes(name))\n continue\n properties[name] = schema\n const role = capability.propRoles[name]\n if (role) propRoles[name] = role\n const limit = capability.textLimits[name]\n if (limit !== undefined) textLimits[name] = limit\n }\n }\n return { properties, propRoles, textLimits }\n}\n\n/**\n * What a node capability declared in core and one carried by the palette\n * disagree about (AGL-3156).\n *\n * `check:ai-palette` pins the generated file to the generator's output for its\n * inputs, which cannot notice a capability leaving those inputs: when repeat\n * stopped being a component's attribute, the generator regenerated cleanly and\n * the palette simply stopped mentioning it. This reads the core declarations\n * instead and asks whether the palette still accounts for each one, so the\n * next generic capability cannot vanish the same way.\n *\n * `offeredFieldKinds` is every field kind the palette DOES carry somewhere,\n * derived from the generated entries rather than restated here: a capability\n * attribute drawn with one of those kinds has no excuse for being omitted.\n */\nexport function auditNodeCapabilities(\n declared: ReadonlyArray<{\n id: string\n label: string\n attributes: ReadonlyArray<{ name: string; component: string }>\n }>,\n carried: Readonly<Record<string, AiNodeCapability>>,\n offeredFieldKinds: ReadonlySet<string>,\n): string[] {\n const problems: string[] = []\n for (const capability of declared) {\n const entry = carried[capability.id]\n if (!entry) {\n problems.push(\n `the palette carries no node capability \"${capability.id}\": a model cannot use it at all`,\n )\n continue\n }\n if (!Object.keys(entry.propsSchema.properties).length) {\n problems.push(\n `node capability \"${capability.id}\" is carried with no prop a model can write`,\n )\n }\n for (const attribute of capability.attributes) {\n if (entry.propsSchema.properties[attribute.name]) continue\n if (!entry.omittedProps.includes(attribute.name)) {\n problems.push(\n `node capability \"${capability.id}\" declares \"${attribute.name}\", which the palette neither offers nor records as omitted`,\n )\n continue\n }\n if (offeredFieldKinds.has(attribute.component)) {\n problems.push(\n `node capability \"${capability.id}\" omits \"${attribute.name}\", whose \"${attribute.component}\" field the palette offers elsewhere`,\n )\n }\n }\n }\n for (const id of Object.keys(carried)) {\n if (!declared.some((capability) => capability.id === id)) {\n problems.push(\n `the palette carries node capability \"${id}\", which core no longer declares`,\n )\n }\n }\n return problems\n}\n\nexport interface AiSurfaceDefinition {\n /** The component id at the top of a tree for this surface. */\n root: string\n /** Every component id a node in this surface may carry. */\n allow: readonly string[]\n /**\n * Component ids admitted only in a tree the platform's own code composed\n * (AGL-3660), never listed in the surface's catalog: see\n * `AiNodeTreeContext.codeBuilt`.\n */\n codeOnly?: readonly string[]\n}\n\n/**\n * Theme vocabulary an `sx` value may name, read off the theme sources so the\n * validator's allowlist is derived rather than typed by hand.\n */\nexport interface AiSxTokens {\n /** Dotted palette paths such as `primary.main`. */\n palette: readonly string[]\n /** Responsive object keys. */\n breakpoints: readonly string[]\n /** Values `typography` may take. */\n typographyVariants: readonly string[]\n}\n\n/**\n * Text ceilings per role of copy (AGL-2905). A headline is one line of a\n * hero, body copy is a paragraph or two, and a button label has to fit a\n * button.\n */\nexport const AI_TEXT_LIMITS = {\n headline: 120,\n body: 2_000,\n button: 40,\n /** A short label, title or caption. */\n label: 200,\n} as const\n\n/**\n * Prompt budget per surface catalog, in characters at four per token.\n * Asserted by a spec so a component that grows its description cannot\n * silently push a system prompt past what the route pays for.\n */\nexport const AI_PALETTE_CATALOG_MAX_TOKENS = 2_500\nexport const AI_PALETTE_CATALOG_MAX_CHARS = AI_PALETTE_CATALOG_MAX_TOKENS * 4\n\n/** The model's own token estimate: characters over four. */\nexport function estimateCatalogTokens(catalog: string): number {\n return Math.ceil(catalog.length / 4)\n}\n\n/**\n * The documents a generator emits (AGL-2935). Each is composed on one\n * surface of the palette and held to its own budget below.\n */\nexport type AiOutputKind =\n | 'page'\n | 'template'\n | 'component'\n | 'layout'\n | 'form'\n | 'email'\n\nexport const AI_OUTPUT_KINDS: readonly AiOutputKind[] = [\n 'page',\n 'template',\n 'component',\n 'layout',\n 'form',\n 'email',\n]\n\n/** The surface each output kind is composed on. */\nexport const AI_OUTPUT_SURFACE: Record<AiOutputKind, AiSurface> = {\n page: 'screen',\n template: 'screen',\n component: 'component',\n layout: 'layout',\n form: 'form',\n email: 'email',\n}\n\n/**\n * What the rule-17 scorer measures on an emitted tree:\n *\n * - `nodes` — how many elements the tree carries;\n * - `bytes` — the stored node map, in the unit the save ceiling uses\n * (`nodeMapBytes`, msgpack);\n * - `imageBytes` — what the placed library images request at the slot's\n * variant, from each asset's recorded size;\n * - `embeds` — third-party players and frames the output loads;\n * - `fontFamilies` — families named beyond the theme's own;\n * - `emailHtmlBytes` — an email's rendered HTML, which is what a mail\n * client clips.\n */\nexport type AiBudgetMetric =\n | 'nodes'\n | 'bytes'\n | 'imageBytes'\n | 'embeds'\n | 'fontFamilies'\n | 'emailHtmlBytes'\n\nexport type AiOutputBudget = Record<Exclude<AiBudgetMetric, 'emailHtmlBytes'>, number> & {\n emailHtmlBytes?: number\n}\n\n/**\n * The size past which Gmail clips a message and hides the rest behind a\n * \"View entire message\" link — the unsubscribe footer included.\n */\nexport const AI_EMAIL_CLIP_BYTES = 102_000\n\n/**\n * The budget per output kind (AGL-2935, rule 17), beside the catalog budget\n * above for the same reason: a generator that grows its output past what\n * its kind is for is caught by a number, not noticed by a reader.\n *\n * A page is a few hundred elements and tens of kilobytes of stored map; a\n * reusable component is one repeated block; a layout is the chrome every\n * page loads, so it carries the least imagery; a form is fields and a\n * button — each field a Form Control over a Form Label and its answer, a\n * choice a Choice Label over its Radio or Checkbox, so a form of a few dozen\n * questions is a hundred-odd elements; an email is held to the clipping threshold on its rendered HTML.\n * No kind names a font beyond the theme's.\n *\n * A third-party player is a page's or a component's to carry, and only where\n * a confirmed plan lists it (rule 16). A template is rendered for every\n * record it serves and a layout around every page, so neither carries one\n * (AGL-3433): an embed there loads a stranger's code on every page of a\n * collection or a site.\n */\nexport const AI_OUTPUT_BUDGETS: Record<AiOutputKind, AiOutputBudget> = {\n page: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n template: {\n nodes: 400,\n bytes: 60_000,\n imageBytes: 1_500_000,\n embeds: 0,\n fontFamilies: 0,\n },\n component: {\n nodes: 80,\n bytes: 12_000,\n imageBytes: 500_000,\n embeds: 1,\n fontFamilies: 0,\n },\n layout: {\n nodes: 160,\n bytes: 24_000,\n imageBytes: 200_000,\n embeds: 0,\n fontFamilies: 0,\n },\n form: {\n nodes: 150,\n bytes: 15_000,\n imageBytes: 0,\n embeds: 0,\n fontFamilies: 0,\n },\n email: {\n nodes: 150,\n bytes: 24_000,\n imageBytes: 1_000_000,\n embeds: 0,\n fontFamilies: 0,\n emailHtmlBytes: AI_EMAIL_CLIP_BYTES,\n },\n}\n\n/**\n * What a first visit to a generated output is estimated to transfer — the\n * weight the proposal shows before anything is applied (rule 17). A page\n * starts from the platform's measured page weight; a component or a layout\n * reports only what it adds to a page; an email is not a page load.\n */\nexport interface AiLoadEstimate {\n /** The measured weight of a published page before this output's own bytes; 0 for a fragment. */\n pageBytes: number\n /** The stored node map, in the save's own bytes. */\n documentBytes: number\n /** What the placed library images request at their slots. */\n imageBytes: number\n /** Images the estimate could not size: no recorded size, or not from the library. */\n imagesUnmeasured: number\n embeds: number\n totalBytes: number\n}\n\n/** Typography variants that read as headlines, for the `children` ceiling. */\nexport function isHeadlineVariant(variant: unknown): boolean {\n return typeof variant === 'string' && /^(display\\w*|h[1-6]|heading)$/.test(variant)\n}\n"],"names":["AI_SURFACE_NAMES","nodeCapabilityProps","capabilities","acceptsChildren","properties","propRoles","textLimits","capability","Object","values","name","schema","entries","propsSchema","childrenOnlyProps","includes","role","limit","undefined","auditNodeCapabilities","declared","carried","offeredFieldKinds","problems","entry","id","push","keys","length","attribute","attributes","omittedProps","has","component","some","AI_TEXT_LIMITS","headline","body","button","label","AI_PALETTE_CATALOG_MAX_TOKENS","AI_PALETTE_CATALOG_MAX_CHARS","estimateCatalogTokens","catalog","Math","ceil","AI_OUTPUT_KINDS","AI_OUTPUT_SURFACE","page","template","layout","form","email","AI_EMAIL_CLIP_BYTES","AI_OUTPUT_BUDGETS","nodes","bytes","imageBytes","embeds","fontFamilies","emailHtmlBytes","isHeadlineVariant","variant","test"],"mappings":"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"}
@@ -32,8 +32,10 @@ import type { AiSystemBlock } from './ai-runtime';
32
32
  * Every answer is held by a check in `tools/ai-products-tool.ts`, which is
33
33
  * where storefront copy safety is enforced — no health, financial or legal
34
34
  * claim, no certification or endorsement the merchant did not state, and no
35
- * price. The rules below say the same, so the model is never refused for a
36
- * rule it was not told.
35
+ * price in any text. The rules below say the same, so the model is never
36
+ * refused for a rule it was not told. A catalog's products each carry a
37
+ * suggested price in a field of its own (Zach, 2026-10-12), which code keeps
38
+ * in range (`aiCatalogPrice`); the copy still states none.
37
39
  */
38
40
  /** The step kind every generation of a `products` job is routed as. */
39
41
  export declare const AI_PRODUCTS_STEP: AiStepKind;
@@ -30,8 +30,10 @@ import { runValidatedGeneration } from "./ai-doctrine.js";
30
30
  * Every answer is held by a check in `tools/ai-products-tool.ts`, which is
31
31
  * where storefront copy safety is enforced — no health, financial or legal
32
32
  * claim, no certification or endorsement the merchant did not state, and no
33
- * price. The rules below say the same, so the model is never refused for a
34
- * rule it was not told.
33
+ * price in any text. The rules below say the same, so the model is never
34
+ * refused for a rule it was not told. A catalog's products each carry a
35
+ * suggested price in a field of its own (Zach, 2026-10-12), which code keeps
36
+ * in range (`aiCatalogPrice`); the copy still states none.
35
37
  */ /** The step kind every generation of a `products` job is routed as. */ export const AI_PRODUCTS_STEP = 'job.products';
36
38
  /**
37
39
  * Each generation's answer ceiling. A catalog is the largest answer the step
@@ -46,8 +48,9 @@ export const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2000;
46
48
  /**
47
49
  * Rules every storefront generation states, because its check refuses an
48
50
  * answer for each of them.
49
- */ const STOREFRONT_RULES = [
50
- 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.',
51
+ */ const STOREFRONT_PRICE_RULE = 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.';
52
+ const STOREFRONT_RULES = [
53
+ STOREFRONT_PRICE_RULE,
51
54
  'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',
52
55
  'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',
53
56
  'Write plain text: no markup, no formatting marks, no emoji, no links.'
@@ -70,10 +73,13 @@ export const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2000;
70
73
  ];
71
74
  /** The rules for a store's first products. Byte-identical on every request. */ export const AI_CATALOG_INSTRUCTIONS = [
72
75
  {
73
- text: 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing and what its photo should show.\n\n' + `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
76
+ text: 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\n\n' + `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
74
77
  'Propose 6 to 12 products that fit the brief, unless the brief says how many.',
75
78
  'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',
76
- ...STOREFRONT_RULES,
79
+ // The price has its own field (Zach, 2026-10-12); the copy states none.
80
+ 'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',
81
+ ...STOREFRONT_RULES.filter((rule)=>rule !== STOREFRONT_PRICE_RULE),
82
+ 'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',
77
83
  `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,
78
84
  'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',
79
85
  'The search title names the product plainly, and neither it nor the search description marks a missing fact.',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.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_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price. The rules below say the same, so the model is never refused for a\n * rule it was not told.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_RULES = [\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.',\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing and what its photo should show.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n ...STOREFRONT_RULES,\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.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_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price in any text. The rules below say the same, so the model is never\n * refused for a rule it was not told. A catalog's products each carry a\n * suggested price in a field of its own (Zach, 2026-10-12), which code keeps\n * in range (`aiCatalogPrice`); the copy still states none.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_PRICE_RULE =\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.'\nconst STOREFRONT_RULES = [\n STOREFRONT_PRICE_RULE,\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n // The price has its own field (Zach, 2026-10-12); the copy states none.\n 'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',\n ...STOREFRONT_RULES.filter((rule) => rule !== STOREFRONT_PRICE_RULE),\n 'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_PRICE_RULE","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","filter","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,4BAA4B,EAC5BC,4BAA4B,EAC5BC,sBAAsB,QAMjB,0BAAsB;AAG7B,SAASC,gBAAgB,QAA+B,0BAAsB;AAC9E,SACEC,eAAe,EACfC,oBAAoB,EACpBC,kBAAkB,EAClBC,uBAAuB,EACvBC,oBAAoB,EACpBC,yBAAyB,EACzBC,cAAc,EACdC,iBAAiB,EACjBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,sBAAsB,QAAoC,mBAAe;AAGlF;;;;;;;;;;;;;;;;CAgBC,GAED,qEAAqE,GACrE,OAAO,MAAMC,mBAA+B,eAAc;AAE1D;;;;;;;CAOC,GACD,OAAO,MAAMC,wBAAwBZ,gBAAgB,CAACW,iBAAiB,CAACE,SAAS,CAAA;AACjF,OAAO,MAAMC,6BAA6B,KAAK;AAC/C,OAAO,MAAMC,mCAAmC,KAAK;AAErD;;;CAGC,GACD,MAAMC,wBACJ;AACF,MAAMC,mBAAmB;IACvBD;IACA;IACA;IACA;CACD;AAED,uEAAuE,GACvE,OAAO,MAAME,+BAAgD;IAC3D;QACEC,MACE,6LACA,CAAC,kBAAkB,EAAEb,0BAA0B,mDAAmD,CAAC,GACnG,aACA;YACE;YACA;eACGW;YACH;YACA;YACA;YACA;YACA;YACA;SACD,CACEG,GAAG,CAAC,CAACC,OAAS,CAAC,EAAE,EAAEA,MAAM,EACzBC,IAAI,CAAC;QACVC,iBAAiB;IACnB;CACD,CAAA;AAED,6EAA6E,GAC7E,OAAO,MAAMC,0BAA2C;IACtD;QACEL,MACE,yOACA,CAAC,kBAAkB,EAAEjB,qBAAqB,mDAAmD,CAAC,GAC9F,aACA;YACE;YACA;YACA,wEAAwE;YACxE;eACGe,iBAAiBQ,MAAM,CAAC,CAACJ,OAASA,SAASL;YAC9C;YACA,CAAC,sFAAsF,EAAEnB,6BAA6B,gDAAgD,CAAC;YACvK;YACA;YACA;SACD,CACEuB,GAAG,CAAC,CAACC,OAAS,CAAC,EAAE,EAAEA,MAAM,EACzBC,IAAI,CAAC;QACVC,iBAAiB;IACnB;CACD,CAAA;AAED,6EAA6E,GAC7E,OAAO,MAAMG,6BAA8C;IACzD;QACEP,MACE,sLACA,CAAC,kBAAkB,EAAEf,wBAAwB,mDAAmD,CAAC,GACjG,aACA;YACE;YACA;YACA;YACA;YACA;YACA;YACA;SACD,CACEgB,GAAG,CAAC,CAACC,OAAS,CAAC,EAAE,EAAEA,MAAM,EACzBC,IAAI,CAAC;QACVC,iBAAiB;IACnB;CACD,CAAA;AAWD,kGAAkG,GAClG,OAAO,SAASI,oBAAoBC,KAA+B;IACjE,MAAM,EAAEC,OAAO,EAAE,GAAGD;IACpB,MAAME,QAAQ;QACZ,CAAC,OAAO,EAAEF,MAAMG,KAAK,IAAI,kBAAkB;QAC3C,CAAC,SAAS,EAAEF,QAAQG,IAAI,EAAE;QAC1B,CAAC,MAAM,EAAEH,QAAQI,IAAI,EAAE;QACvB;QACAJ,QAAQV,IAAI,CAACe,IAAI,MAAM;QACvB,CAAC,cAAc,EAAEL,QAAQM,IAAI,CAACC,MAAM,GAAGP,QAAQM,IAAI,CAACb,IAAI,CAAC,QAAQ,UAAU;KAC5E;IACD,IAAIO,QAAQQ,OAAO,CAACD,MAAM,EAAE;QAC1BN,MAAMQ,IAAI,CAAC,eAAeT,QAAQQ,OAAO,CAACjB,GAAG,CAAC,CAACmB,SAAW,CAAC,EAAE,EAAEA,OAAOP,IAAI,CAAC,EAAE,EAAEO,OAAOC,MAAM,CAAClB,IAAI,CAAC,OAAO;IAC3G,OAAO;QACLQ,MAAMQ,IAAI,CAAC;IACb;IACA,IAAIT,QAAQY,QAAQ,IAAIZ,QAAQa,cAAc,EAAE;QAC9CZ,MAAMQ,IAAI,CACR,CAAC,sBAAsB,EAAET,QAAQY,QAAQ,IAAI,UAAU,EACvD,CAAC,4BAA4B,EAAEZ,QAAQa,cAAc,IAAI,UAAU;IAEvE;IACA,MAAMC,aAAaf,MAAMe,UAAU,CAACC,KAAK,CAAC,GAAG9C;IAC7CgC,MAAMQ,IAAI,CACRK,WAAWP,MAAM,GAAG,+BAA+B,wCAChDO,WAAWvB,GAAG,CAAC,CAACyB,WAAa,CAAC,EAAE,EAAEA,SAASC,EAAE,CAAC,EAAE,EAAED,SAASb,IAAI,EAAE,GACpE,CAAC,OAAO,EAAEJ,MAAMmB,KAAK,GAAG,aAAa,QAAQ;IAE/C,OAAOjB,MAAMR,IAAI,CAAC;AACpB;AAeA;;;;;CAKC,GACD,OAAO,SAAS0B,sBAAsBpB,KAAiC;QACxCA,cAYhBA;IAZb,MAAMqB,QAAQrB,MAAMmB,KAAK,IAAInB,eAAAA,MAAMqB,KAAK,YAAXrB,eAAe,OAAQ;IACpD,MAAMsB,UAA2B;WAC3BD,QAAQ;YAACA;SAAM,GAAG,EAAE;QACxB;YAAEhB,MAAM;YAAQd,MAAMQ,oBAAoB,aAAKC;gBAAOmB,OAAOI,QAAQF;;QAAU;KAChF;IACD,OAAOvC,uBAAuB,gBAAgB;QAC5C0C,MAAMzC;OACFiB,MAAMyB,QAAQ,GAAG;QAAEA,UAAUzB,MAAMyB,QAAQ;IAAC,IAAI,CAAC,GACjDzB,MAAM0B,KAAK,GAAG;QAAEA,OAAO1B,MAAM0B,KAAK;IAAC,IAAI,CAAC;QAC5CC,cAAcrC;QACdsC,UAAU;YAAC;gBAAEC,MAAM;gBAAQP,SAASD,QAAQC,UAAUvB,oBAAoB,aAAKC;oBAAOmB,OAAO;;YAAS;SAAE;QACxGW,MAAMrD;QACNQ,SAAS,GAAEe,mBAAAA,MAAMf,SAAS,YAAfe,mBAAmBd;OAC3B6C,cACC/B,MAAMgC,MAAM,GAAG;QAAEA,QAAQhC,MAAMgC,MAAM;IAAC,IAAI,CAAC,GAC3ChC,MAAMiC,QAAQ,GAAG;QAAEA,UAAUjC,MAAMiC,QAAQ;IAAC,IAAI,CAAC;QACrDC,OAAO,CAACC,SACNtD,mBAAmBsD,QAAQ;gBACzBC,aAAapC,MAAMe,UAAU,CAACC,KAAK,CAAC,GAAG9C,8BAA8BsB,GAAG,CAAC,CAACyB,WAAaA,SAASC,EAAE;gBAClGmB,aAAarC,MAAMC,OAAO,CAACQ,OAAO,CAACD,MAAM;gBACzC8B,eAAenE,uBAAuB6B,MAAMC,OAAO;YACrD;;AAEN;AAOA,0DAA0D,GAC1D,OAAO,SAASsC,gBAAgBvC,KAAsD;IACpF,OAAO;QAAC,CAAC,OAAO,EAAEA,MAAMG,KAAK,IAAI,kBAAkB;QAAE;QAAUH,MAAMwC,KAAK,CAAClC,IAAI;KAAG,CAACZ,IAAI,CAAC;AAC1F;AAEA,sEAAsE,GACtE,OAAO,SAAS+C,kBAAkBzC,KAAqD;IACrF,OAAOlB,uBAAuB,WAAW;QACvC0C,MAAMzC;OACFiB,MAAMyB,QAAQ,GAAG;QAAEA,UAAUzB,MAAMyB,QAAQ;IAAC,IAAI,CAAC,GACjDzB,MAAM0B,KAAK,GAAG;QAAEA,OAAO1B,MAAM0B,KAAK;IAAC,IAAI,CAAC;QAC5CC,cAAc/B;QACdgC,UAAU;YAAC;gBAAEC,MAAM;gBAAQP,SAASiB,gBAAgBvC;YAAO;SAAE;QAC7D8B,MAAMzD;QACNY,WAAWe,MAAMf,SAAS;OACvB8C,cACC/B,MAAMgC,MAAM,GAAG;QAAEA,QAAQhC,MAAMgC,MAAM;IAAC,IAAI,CAAC,GAC3ChC,MAAMiC,QAAQ,GAAG;QAAEA,UAAUjC,MAAMiC,QAAQ;IAAC,IAAI,CAAC;QACrDC,OAAO,CAACC,SAAWxD,eAAewD,QAAQ;gBAAEG,eAAetC,MAAMwC,KAAK;YAAC;;AAE3E;AASA,uFAAuF,GACvF,OAAO,SAASE,mBAAmB1C,KAAmF;IACpH,MAAM2C,WAAW3C,MAAM4C,qBAAqB,CAAC5B,KAAK,CAAC,GAAG9C;IACtD,OAAO;QACL,CAAC,OAAO,EAAE8B,MAAMG,KAAK,IAAI,kBAAkB;QAC3C,CAAC,kCAAkC,EAAEwC,SAASnC,MAAM,GAAGmC,SAASjD,IAAI,CAAC,QAAQ,UAAU;QACvF;QACAM,MAAMwC,KAAK,CAAClC,IAAI;KACjB,CAACZ,IAAI,CAAC;AACT;AAEA,sEAAsE,GACtE,OAAO,SAASmD,qBACd7C,KAAwD;IAExD,OAAOlB,uBAAuB,kCAAkC;QAC9D0C,MAAMzC;OACFiB,MAAMyB,QAAQ,GAAG;QAAEA,UAAUzB,MAAMyB,QAAQ;IAAC,IAAI,CAAC,GACjDzB,MAAM0B,KAAK,GAAG;QAAEA,OAAO1B,MAAM0B,KAAK;IAAC,IAAI,CAAC;QAC5CC,cAAc7B;QACd8B,UAAU;YAAC;gBAAEC,MAAM;gBAAQP,SAASoB,mBAAmB1C;YAAO;SAAE;QAChE8B,MAAMvD;QACNU,WAAWe,MAAMf,SAAS;OACvB8C,cACC/B,MAAMgC,MAAM,GAAG;QAAEA,QAAQhC,MAAMgC,MAAM;IAAC,IAAI,CAAC,GAC3ChC,MAAMiC,QAAQ,GAAG;QAAEA,UAAUjC,MAAMiC,QAAQ;IAAC,IAAI,CAAC;QACrDC,OAAO,CAACC,SACNvD,kBAAkBuD,QAAQ;gBACxBS,uBAAuB5C,MAAM4C,qBAAqB;gBAClDN,eAAetC,MAAMwC,KAAK;YAC5B;;AAEN;AAEA,8EAA8E,GAC9E,SAAST;IACP,MAAMe,MAAM1E,gBAAgB,CAACW,iBAAiB;IAC9C,OAAO,aACD+D,IAAIC,QAAQ,GAAG;QAAEA,UAAUD,IAAIC,QAAQ;IAAC,IAAI,CAAC,GAC7CD,IAAIE,MAAM,GAAG;QAAEA,QAAQF,IAAIE,MAAM;IAAC,IAAI,CAAC;AAE/C"}
@@ -14,14 +14,20 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { STARTER_TEMPLATES } from "@aglyn/aglyn/app-utils/starter-templates";
17
+ import { FORM_INPUT_ELEMENT_IDS } from "@aglyn/aglyn/app-utils/forms";
17
18
  import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
18
19
  import { validateAiDoctrineTree } from "./ai-doctrine-validators.js";
19
20
  import { isHeadlineVariant } from "./ai-palette.js";
20
21
  import { AI_PALETTE, AI_SURFACES } from "./ai-palette.generated.js";
21
22
  /** The block's ceiling: 1,500 tokens at four characters a token. */ export const AI_TEMPLATE_EXAMPLES_MAX_CHARS = 6000;
22
- const FORM_COMPONENTS = new Set([
23
+ /**
24
+ * A form and every piece of one — the all-in-one Form Field and MUI's
25
+ * separate form elements — which a page never draws: it places a saved form.
26
+ */ const FORM_COMPONENTS = new Set([
23
27
  'form',
24
- 'formField'
28
+ 'formField',
29
+ ...Object.values(FORM_INPUT_ELEMENT_IDS),
30
+ 'muiFormHelperText'
25
31
  ]);
26
32
  /** The elements a page links with, which go where their `screenId` or `href` says. */ const LINK_COMPONENTS = new Set([
27
33
  'muiButton',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-template-examples.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 STARTER_TEMPLATES,\n type StarterTemplate,\n} from '@aglyn/aglyn/app-utils/starter-templates'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport {\n validateAiDoctrineTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n} from './ai-doctrine-validators'\nimport { isHeadlineVariant } from './ai-palette'\nimport { AI_PALETTE, AI_SURFACES } from './ai-palette.generated'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * The page examples a template generation is shown (AGL-2909): pages from\n * the platform's own starter sites (`STARTER_TEMPLATES`), in the node-map\n * shape the tool takes, as one cached block that is the same bytes for every\n * org.\n *\n * Three things about a starter predate the building rules, and every example\n * is brought up to them the same way rather than shown as it stands:\n *\n * - HEADINGS. A starter picks a heading's look with its variant, and rule 11\n * reads its level from the element. The page's first heading renders as\n * its h1, each later one as an h2, and a sixth-level subtitle as a\n * paragraph.\n * - FORMS. A starter draws its contact form inline, and rule 3 places a form\n * built on the Forms page by id, so the form is left out.\n * - LINKS. A starter's call to action points nowhere until a member picks\n * where it goes, and rule 10 refuses a Button or a Screen Link with no\n * destination (AGL-3072), so such a link is left out.\n *\n * Only props the palette declares are kept, since the model is held to the\n * same palette, and none that holds a px, rem or em length, which rule 5\n * names as a length never to write. A page that still breaks a rule, that\n * the palette validator would change in any way, or that places an element\n * the page surface does not offer (a commerce block) is not shown, and two\n * pages of one shape are shown once: an example the model imitates is one\n * the doctrine accepts exactly as written.\n */\n\nexport interface AiTemplateExample {\n /** The starter site's name. */\n starter: string\n /** The page's name within it. */\n page: string\n tree: AiDoctrineTree\n}\n\n/** The block's ceiling: 1,500 tokens at four characters a token. */\nexport const AI_TEMPLATE_EXAMPLES_MAX_CHARS = 6_000\n\nconst FORM_COMPONENTS = new Set(['form', 'formField'])\n\n/** The elements a page links with, which go where their `screenId` or `href` says. */\nconst LINK_COMPONENTS = new Set(['muiButton', 'muiScreenLink'])\n\n/** Whether a starter node is a link that names nowhere to go. */\nfunction linksNowhere(componentId: string, props: Record<string, unknown> | undefined): boolean {\n if (!LINK_COMPONENTS.has(componentId)) return false\n const named = (value: unknown) => typeof value === 'string' && value.trim() !== ''\n return !named(props?.['screenId']) && !named(props?.['href'])\n}\n\n/** A CSS length in the units rule 5 forbids. */\nconst CSS_LENGTH = /\\d(?:\\.\\d+)?(?:px|rem|em)\\b/i\n\ntype StarterNode = {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: unknown\n}\n\n/** A starter page as the tool's node map, brought up to the rules above; `null` when it cannot be shown. */\nexport function aiStarterExampleTree(nodes: Record<string, StarterNode>): AiDoctrineTree | null {\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) return null\n const allowed = new Set(AI_SURFACES.screen.allow)\n const out: Record<string, AiDoctrineNode> = {}\n let headings = 0\n let unavailable = false\n const visit = (id: string): string | null => {\n const node = nodes[id]\n const componentId = String(node?.componentId ?? '')\n if (!node || FORM_COMPONENTS.has(componentId) || linksNowhere(componentId, node.props)) return null\n const root = id === CANVAS_ROOT_ELEMENT_ID\n if (!root && !allowed.has(componentId)) {\n unavailable = true\n return null\n }\n // Inserted before the children, so the map reads top-down.\n const shaped: AiDoctrineNode = { componentId: root ? 'div' : componentId }\n out[id] = shaped\n const declared = AI_PALETTE[componentId]?.propsSchema.properties ?? {}\n const props: Record<string, unknown> = {}\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!declared[name] || value === undefined) continue\n if (typeof value === 'string' && CSS_LENGTH.test(value)) continue\n props[name] = value\n }\n if (componentId === 'muiTypography') {\n const variant = props['variant']\n if (variant === 'h6') props['component'] = 'p'\n else if (isHeadlineVariant(variant)) props['component'] = headings++ === 0 ? 'h1' : 'h2'\n }\n if (Object.keys(props).length) shaped.props = props\n if (node.sx && Object.keys(node.sx).length) shaped.sx = node.sx\n const children = (Array.isArray(node.nodes) ? (node.nodes as unknown[]) : [])\n .map((child) => (typeof child === 'string' ? visit(child) : null))\n .filter((child): child is string => child !== null)\n if (children.length) shaped.nodes = children\n return id\n }\n visit(CANVAS_ROOT_ELEMENT_ID)\n return unavailable ? null : { rootId: CANVAS_ROOT_ELEMENT_ID, nodes: out }\n}\n\n/** A tree's element structure without its copy or ids, to show one shape once. */\nfunction shapeOf(tree: AiDoctrineTree, id = tree.rootId): string {\n const node = tree.nodes[id]\n return `${node.componentId}(${(node.nodes ?? []).map((child) => shapeOf(tree, child)).join(',')})`\n}\n\n/** Every starter page the doctrine accepts as a template exactly as the example shows it. */\nexport function aiStarterTemplateExamples(\n starters: readonly StarterTemplate[] = STARTER_TEMPLATES,\n): AiTemplateExample[] {\n const examples: AiTemplateExample[] = []\n const shapes = new Set<string>()\n for (const starter of starters) {\n for (const screen of starter.screens) {\n const tree = aiStarterExampleTree(screen.nodes as Record<string, StarterNode>)\n if (!tree) continue\n const report = validateAiDoctrineTree(tree, 'template')\n if (!report.ok || !report.tree || report.tree.repairs.length) continue\n const shape = shapeOf(tree)\n if (shapes.has(shape)) continue\n shapes.add(shape)\n examples.push({ starter: starter.displayName, page: screen.displayName, tree })\n }\n }\n return examples\n}\n\nexport const AI_TEMPLATE_EXAMPLES_PREAMBLE = [\n 'Examples: pages from the platform’s starter sites, in the node-map shape the tool takes.',\n 'Build the way they are built: each band a Container at a stock width (xl for a marketing band, lg for an interactive one, md for prose) with vertical padding on the spacing scale, its content in a Stack, and one h1 for the page.',\n 'Their copy is the starter’s own; a template binds its subject’s tokens wherever a record’s own words, links or pictures go.',\n].join(' ')\n\n/** The examples as one cached system block, held to its ceiling an example at a time. */\nexport function aiTemplateExamplesBlock(\n examples: readonly AiTemplateExample[] = aiStarterTemplateExamples(),\n): AiSystemBlock {\n const parts = [AI_TEMPLATE_EXAMPLES_PREAMBLE]\n let used = AI_TEMPLATE_EXAMPLES_PREAMBLE.length\n for (const example of examples) {\n const text = `Starter \"${example.starter}\", page \"${example.page}\":\\n${JSON.stringify(example.tree)}`\n if (used + text.length + 2 > AI_TEMPLATE_EXAMPLES_MAX_CHARS) continue\n parts.push(text)\n used += text.length + 2\n }\n return { text: parts.join('\\n\\n'), cacheBreakpoint: true }\n}\n\nlet examplesBlock: AiSystemBlock | null = null\n\n/** The block a template generation sends, built once per process from the starters. */\nexport function aiTemplateExamplesSystemBlock(): AiSystemBlock {\n examplesBlock ??= aiTemplateExamplesBlock()\n return examplesBlock\n}\n"],"names":["STARTER_TEMPLATES","CANVAS_ROOT_ELEMENT_ID","validateAiDoctrineTree","isHeadlineVariant","AI_PALETTE","AI_SURFACES","AI_TEMPLATE_EXAMPLES_MAX_CHARS","FORM_COMPONENTS","Set","LINK_COMPONENTS","linksNowhere","componentId","props","has","named","value","trim","CSS_LENGTH","aiStarterExampleTree","nodes","allowed","screen","allow","out","headings","unavailable","visit","id","node","String","root","shaped","declared","propsSchema","properties","name","Object","entries","undefined","test","variant","keys","length","sx","children","Array","isArray","map","child","filter","rootId","shapeOf","tree","join","aiStarterTemplateExamples","starters","examples","shapes","starter","screens","report","ok","repairs","shape","add","push","displayName","page","AI_TEMPLATE_EXAMPLES_PREAMBLE","aiTemplateExamplesBlock","parts","used","example","text","JSON","stringify","cacheBreakpoint","examplesBlock","aiTemplateExamplesSystemBlock"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,iBAAiB,QAEZ,2CAA0C;AACjD,SAASC,sBAAsB,QAAQ,2CAA0C;AACjF,SACEC,sBAAsB,QAGjB,8BAA0B;AACjC,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SAASC,UAAU,EAAEC,WAAW,QAAQ,4BAAwB;AAuChE,kEAAkE,GAClE,OAAO,MAAMC,iCAAiC,KAAK;AAEnD,MAAMC,kBAAkB,IAAIC,IAAI;IAAC;IAAQ;CAAY;AAErD,oFAAoF,GACpF,MAAMC,kBAAkB,IAAID,IAAI;IAAC;IAAa;CAAgB;AAE9D,+DAA+D,GAC/D,SAASE,aAAaC,WAAmB,EAAEC,KAA0C;IACnF,IAAI,CAACH,gBAAgBI,GAAG,CAACF,cAAc,OAAO;IAC9C,MAAMG,QAAQ,CAACC,QAAmB,OAAOA,UAAU,YAAYA,MAAMC,IAAI,OAAO;IAChF,OAAO,CAACF,MAAMF,yBAAAA,KAAO,CAAC,WAAW,KAAK,CAACE,MAAMF,yBAAAA,KAAO,CAAC,OAAO;AAC9D;AAEA,8CAA8C,GAC9C,MAAMK,aAAa;AASnB,0GAA0G,GAC1G,OAAO,SAASC,qBAAqBC,KAAkC;IACrE,IAAI,CAACA,KAAK,CAAClB,uBAAuB,EAAE,OAAO;IAC3C,MAAMmB,UAAU,IAAIZ,IAAIH,YAAYgB,MAAM,CAACC,KAAK;IAChD,MAAMC,MAAsC,CAAC;IAC7C,IAAIC,WAAW;IACf,IAAIC,cAAc;IAClB,MAAMC,QAAQ,CAACC;yBAc8BC;YAF1BxB;QAXjB,MAAMwB,OAAOT,KAAK,CAACQ,GAAG;QACtB,MAAMhB,cAAckB,eAAOD,wBAAAA,KAAMjB,WAAW,mBAAI;QAChD,IAAI,CAACiB,QAAQrB,gBAAgBM,GAAG,CAACF,gBAAgBD,aAAaC,aAAaiB,KAAKhB,KAAK,GAAG,OAAO;QAC/F,MAAMkB,OAAOH,OAAO1B;QACpB,IAAI,CAAC6B,QAAQ,CAACV,QAAQP,GAAG,CAACF,cAAc;YACtCc,cAAc;YACd,OAAO;QACT;QACA,2DAA2D;QAC3D,MAAMM,SAAyB;YAAEpB,aAAamB,OAAO,QAAQnB;QAAY;QACzEY,GAAG,CAACI,GAAG,GAAGI;QACV,MAAMC,qBAAW5B,0BAAAA,UAAU,CAACO,YAAY,qBAAvBP,wBAAyB6B,WAAW,CAACC,UAAU,oBAAI,CAAC;QACrE,MAAMtB,QAAiC,CAAC;QACxC,KAAK,MAAM,CAACuB,MAAMpB,MAAM,IAAIqB,OAAOC,OAAO,EAACT,cAAAA,KAAKhB,KAAK,YAAVgB,cAAc,CAAC,GAAI;YAC5D,IAAI,CAACI,QAAQ,CAACG,KAAK,IAAIpB,UAAUuB,WAAW;YAC5C,IAAI,OAAOvB,UAAU,YAAYE,WAAWsB,IAAI,CAACxB,QAAQ;YACzDH,KAAK,CAACuB,KAAK,GAAGpB;QAChB;QACA,IAAIJ,gBAAgB,iBAAiB;YACnC,MAAM6B,UAAU5B,KAAK,CAAC,UAAU;YAChC,IAAI4B,YAAY,MAAM5B,KAAK,CAAC,YAAY,GAAG;iBACtC,IAAIT,kBAAkBqC,UAAU5B,KAAK,CAAC,YAAY,GAAGY,eAAe,IAAI,OAAO;QACtF;QACA,IAAIY,OAAOK,IAAI,CAAC7B,OAAO8B,MAAM,EAAEX,OAAOnB,KAAK,GAAGA;QAC9C,IAAIgB,KAAKe,EAAE,IAAIP,OAAOK,IAAI,CAACb,KAAKe,EAAE,EAAED,MAAM,EAAEX,OAAOY,EAAE,GAAGf,KAAKe,EAAE;QAC/D,MAAMC,WAAW,AAACC,CAAAA,MAAMC,OAAO,CAAClB,KAAKT,KAAK,IAAKS,KAAKT,KAAK,GAAiB,EAAE,AAAD,EACxE4B,GAAG,CAAC,CAACC,QAAW,OAAOA,UAAU,WAAWtB,MAAMsB,SAAS,MAC3DC,MAAM,CAAC,CAACD,QAA2BA,UAAU;QAChD,IAAIJ,SAASF,MAAM,EAAEX,OAAOZ,KAAK,GAAGyB;QACpC,OAAOjB;IACT;IACAD,MAAMzB;IACN,OAAOwB,cAAc,OAAO;QAAEyB,QAAQjD;QAAwBkB,OAAOI;IAAI;AAC3E;AAEA,gFAAgF,GAChF,SAAS4B,QAAQC,IAAoB,EAAEzB,KAAKyB,KAAKF,MAAM;QAEtBtB;IAD/B,MAAMA,OAAOwB,KAAKjC,KAAK,CAACQ,GAAG;IAC3B,OAAO,GAAGC,KAAKjB,WAAW,CAAC,CAAC,EAAE,EAACiB,cAAAA,KAAKT,KAAK,YAAVS,cAAc,EAAE,EAAEmB,GAAG,CAAC,CAACC,QAAUG,QAAQC,MAAMJ,QAAQK,IAAI,CAAC,KAAK,CAAC,CAAC;AACpG;AAEA,2FAA2F,GAC3F,OAAO,SAASC,0BACdC,WAAuCvD,iBAAiB;IAExD,MAAMwD,WAAgC,EAAE;IACxC,MAAMC,SAAS,IAAIjD;IACnB,KAAK,MAAMkD,WAAWH,SAAU;QAC9B,KAAK,MAAMlC,UAAUqC,QAAQC,OAAO,CAAE;YACpC,MAAMP,OAAOlC,qBAAqBG,OAAOF,KAAK;YAC9C,IAAI,CAACiC,MAAM;YACX,MAAMQ,SAAS1D,uBAAuBkD,MAAM;YAC5C,IAAI,CAACQ,OAAOC,EAAE,IAAI,CAACD,OAAOR,IAAI,IAAIQ,OAAOR,IAAI,CAACU,OAAO,CAACpB,MAAM,EAAE;YAC9D,MAAMqB,QAAQZ,QAAQC;YACtB,IAAIK,OAAO5C,GAAG,CAACkD,QAAQ;YACvBN,OAAOO,GAAG,CAACD;YACXP,SAASS,IAAI,CAAC;gBAAEP,SAASA,QAAQQ,WAAW;gBAAEC,MAAM9C,OAAO6C,WAAW;gBAAEd;YAAK;QAC/E;IACF;IACA,OAAOI;AACT;AAEA,OAAO,MAAMY,gCAAgC;IAC3C;IACA;IACA;CACD,CAACf,IAAI,CAAC,KAAI;AAEX,uFAAuF,GACvF,OAAO,SAASgB,wBACdb,WAAyCF,2BAA2B;IAEpE,MAAMgB,QAAQ;QAACF;KAA8B;IAC7C,IAAIG,OAAOH,8BAA8B1B,MAAM;IAC/C,KAAK,MAAM8B,WAAWhB,SAAU;QAC9B,MAAMiB,OAAO,CAAC,SAAS,EAAED,QAAQd,OAAO,CAAC,SAAS,EAAEc,QAAQL,IAAI,CAAC,IAAI,EAAEO,KAAKC,SAAS,CAACH,QAAQpB,IAAI,GAAG;QACrG,IAAImB,OAAOE,KAAK/B,MAAM,GAAG,IAAIpC,gCAAgC;QAC7DgE,MAAML,IAAI,CAACQ;QACXF,QAAQE,KAAK/B,MAAM,GAAG;IACxB;IACA,OAAO;QAAE+B,MAAMH,MAAMjB,IAAI,CAAC;QAASuB,iBAAiB;IAAK;AAC3D;AAEA,IAAIC,gBAAsC;AAE1C,qFAAqF,GACrF,OAAO,SAASC;IACdD,wBAAAA,gBAAAA,gBAAkBR;IAClB,OAAOQ;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-template-examples.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 STARTER_TEMPLATES,\n type StarterTemplate,\n} from '@aglyn/aglyn/app-utils/starter-templates'\nimport { FORM_INPUT_ELEMENT_IDS } from '@aglyn/aglyn/app-utils/forms'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport {\n validateAiDoctrineTree,\n type AiDoctrineNode,\n type AiDoctrineTree,\n} from './ai-doctrine-validators'\nimport { isHeadlineVariant } from './ai-palette'\nimport { AI_PALETTE, AI_SURFACES } from './ai-palette.generated'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * The page examples a template generation is shown (AGL-2909): pages from\n * the platform's own starter sites (`STARTER_TEMPLATES`), in the node-map\n * shape the tool takes, as one cached block that is the same bytes for every\n * org.\n *\n * Three things about a starter predate the building rules, and every example\n * is brought up to them the same way rather than shown as it stands:\n *\n * - HEADINGS. A starter picks a heading's look with its variant, and rule 11\n * reads its level from the element. The page's first heading renders as\n * its h1, each later one as an h2, and a sixth-level subtitle as a\n * paragraph.\n * - FORMS. A starter draws its contact form inline, and rule 3 places a form\n * built on the Forms page by id, so the form is left out.\n * - LINKS. A starter's call to action points nowhere until a member picks\n * where it goes, and rule 10 refuses a Button or a Screen Link with no\n * destination (AGL-3072), so such a link is left out.\n *\n * Only props the palette declares are kept, since the model is held to the\n * same palette, and none that holds a px, rem or em length, which rule 5\n * names as a length never to write. A page that still breaks a rule, that\n * the palette validator would change in any way, or that places an element\n * the page surface does not offer (a commerce block) is not shown, and two\n * pages of one shape are shown once: an example the model imitates is one\n * the doctrine accepts exactly as written.\n */\n\nexport interface AiTemplateExample {\n /** The starter site's name. */\n starter: string\n /** The page's name within it. */\n page: string\n tree: AiDoctrineTree\n}\n\n/** The block's ceiling: 1,500 tokens at four characters a token. */\nexport const AI_TEMPLATE_EXAMPLES_MAX_CHARS = 6_000\n\n/**\n * A form and every piece of one — the all-in-one Form Field and MUI's\n * separate form elements — which a page never draws: it places a saved form.\n */\nconst FORM_COMPONENTS = new Set<string>([\n 'form',\n 'formField',\n ...Object.values(FORM_INPUT_ELEMENT_IDS),\n 'muiFormHelperText',\n])\n\n/** The elements a page links with, which go where their `screenId` or `href` says. */\nconst LINK_COMPONENTS = new Set(['muiButton', 'muiScreenLink'])\n\n/** Whether a starter node is a link that names nowhere to go. */\nfunction linksNowhere(componentId: string, props: Record<string, unknown> | undefined): boolean {\n if (!LINK_COMPONENTS.has(componentId)) return false\n const named = (value: unknown) => typeof value === 'string' && value.trim() !== ''\n return !named(props?.['screenId']) && !named(props?.['href'])\n}\n\n/** A CSS length in the units rule 5 forbids. */\nconst CSS_LENGTH = /\\d(?:\\.\\d+)?(?:px|rem|em)\\b/i\n\ntype StarterNode = {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: unknown\n}\n\n/** A starter page as the tool's node map, brought up to the rules above; `null` when it cannot be shown. */\nexport function aiStarterExampleTree(nodes: Record<string, StarterNode>): AiDoctrineTree | null {\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) return null\n const allowed = new Set(AI_SURFACES.screen.allow)\n const out: Record<string, AiDoctrineNode> = {}\n let headings = 0\n let unavailable = false\n const visit = (id: string): string | null => {\n const node = nodes[id]\n const componentId = String(node?.componentId ?? '')\n if (!node || FORM_COMPONENTS.has(componentId) || linksNowhere(componentId, node.props)) return null\n const root = id === CANVAS_ROOT_ELEMENT_ID\n if (!root && !allowed.has(componentId)) {\n unavailable = true\n return null\n }\n // Inserted before the children, so the map reads top-down.\n const shaped: AiDoctrineNode = { componentId: root ? 'div' : componentId }\n out[id] = shaped\n const declared = AI_PALETTE[componentId]?.propsSchema.properties ?? {}\n const props: Record<string, unknown> = {}\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!declared[name] || value === undefined) continue\n if (typeof value === 'string' && CSS_LENGTH.test(value)) continue\n props[name] = value\n }\n if (componentId === 'muiTypography') {\n const variant = props['variant']\n if (variant === 'h6') props['component'] = 'p'\n else if (isHeadlineVariant(variant)) props['component'] = headings++ === 0 ? 'h1' : 'h2'\n }\n if (Object.keys(props).length) shaped.props = props\n if (node.sx && Object.keys(node.sx).length) shaped.sx = node.sx\n const children = (Array.isArray(node.nodes) ? (node.nodes as unknown[]) : [])\n .map((child) => (typeof child === 'string' ? visit(child) : null))\n .filter((child): child is string => child !== null)\n if (children.length) shaped.nodes = children\n return id\n }\n visit(CANVAS_ROOT_ELEMENT_ID)\n return unavailable ? null : { rootId: CANVAS_ROOT_ELEMENT_ID, nodes: out }\n}\n\n/** A tree's element structure without its copy or ids, to show one shape once. */\nfunction shapeOf(tree: AiDoctrineTree, id = tree.rootId): string {\n const node = tree.nodes[id]\n return `${node.componentId}(${(node.nodes ?? []).map((child) => shapeOf(tree, child)).join(',')})`\n}\n\n/** Every starter page the doctrine accepts as a template exactly as the example shows it. */\nexport function aiStarterTemplateExamples(\n starters: readonly StarterTemplate[] = STARTER_TEMPLATES,\n): AiTemplateExample[] {\n const examples: AiTemplateExample[] = []\n const shapes = new Set<string>()\n for (const starter of starters) {\n for (const screen of starter.screens) {\n const tree = aiStarterExampleTree(screen.nodes as Record<string, StarterNode>)\n if (!tree) continue\n const report = validateAiDoctrineTree(tree, 'template')\n if (!report.ok || !report.tree || report.tree.repairs.length) continue\n const shape = shapeOf(tree)\n if (shapes.has(shape)) continue\n shapes.add(shape)\n examples.push({ starter: starter.displayName, page: screen.displayName, tree })\n }\n }\n return examples\n}\n\nexport const AI_TEMPLATE_EXAMPLES_PREAMBLE = [\n 'Examples: pages from the platform’s starter sites, in the node-map shape the tool takes.',\n 'Build the way they are built: each band a Container at a stock width (xl for a marketing band, lg for an interactive one, md for prose) with vertical padding on the spacing scale, its content in a Stack, and one h1 for the page.',\n 'Their copy is the starter’s own; a template binds its subject’s tokens wherever a record’s own words, links or pictures go.',\n].join(' ')\n\n/** The examples as one cached system block, held to its ceiling an example at a time. */\nexport function aiTemplateExamplesBlock(\n examples: readonly AiTemplateExample[] = aiStarterTemplateExamples(),\n): AiSystemBlock {\n const parts = [AI_TEMPLATE_EXAMPLES_PREAMBLE]\n let used = AI_TEMPLATE_EXAMPLES_PREAMBLE.length\n for (const example of examples) {\n const text = `Starter \"${example.starter}\", page \"${example.page}\":\\n${JSON.stringify(example.tree)}`\n if (used + text.length + 2 > AI_TEMPLATE_EXAMPLES_MAX_CHARS) continue\n parts.push(text)\n used += text.length + 2\n }\n return { text: parts.join('\\n\\n'), cacheBreakpoint: true }\n}\n\nlet examplesBlock: AiSystemBlock | null = null\n\n/** The block a template generation sends, built once per process from the starters. */\nexport function aiTemplateExamplesSystemBlock(): AiSystemBlock {\n examplesBlock ??= aiTemplateExamplesBlock()\n return examplesBlock\n}\n"],"names":["STARTER_TEMPLATES","FORM_INPUT_ELEMENT_IDS","CANVAS_ROOT_ELEMENT_ID","validateAiDoctrineTree","isHeadlineVariant","AI_PALETTE","AI_SURFACES","AI_TEMPLATE_EXAMPLES_MAX_CHARS","FORM_COMPONENTS","Set","Object","values","LINK_COMPONENTS","linksNowhere","componentId","props","has","named","value","trim","CSS_LENGTH","aiStarterExampleTree","nodes","allowed","screen","allow","out","headings","unavailable","visit","id","node","String","root","shaped","declared","propsSchema","properties","name","entries","undefined","test","variant","keys","length","sx","children","Array","isArray","map","child","filter","rootId","shapeOf","tree","join","aiStarterTemplateExamples","starters","examples","shapes","starter","screens","report","ok","repairs","shape","add","push","displayName","page","AI_TEMPLATE_EXAMPLES_PREAMBLE","aiTemplateExamplesBlock","parts","used","example","text","JSON","stringify","cacheBreakpoint","examplesBlock","aiTemplateExamplesSystemBlock"],"mappings":"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"}
@@ -61,6 +61,18 @@ export declare function aiStarterHomeCandidate(host: Readonly<Record<string, unk
61
61
  * page — a plan keeps it.
62
62
  */
63
63
  export declare function aiStarterHomeUntouched(publishedVersionId: string, versionIds: readonly string[]): boolean;
64
+ /**
65
+ * The record page a site already serves for a dataset (AGL-3747), from the
66
+ * `recordPage` fact the data plugin's index answers for a site: its base and
67
+ * the address field its pages are reached by. `null` where there is none, or
68
+ * where its address field is no longer one of the dataset's fields.
69
+ */
70
+ export declare function aiInventoryRecordPage(fact: unknown, fields: ReadonlyArray<{
71
+ id: string;
72
+ }>): {
73
+ recordBase: string;
74
+ addressField: string;
75
+ } | null;
64
76
  /**
65
77
  * Read one site's inventory for a generation.
66
78
  *
@@ -101,6 +101,22 @@ async function readWindow(query, fields, cap, map) {
101
101
  */ export function aiStarterHomeUntouched(publishedVersionId, versionIds) {
102
102
  return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId;
103
103
  }
104
+ /** A record template's base, as the data plugin keeps it: one to four URL-safe segments. */ const RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/;
105
+ /**
106
+ * The record page a site already serves for a dataset (AGL-3747), from the
107
+ * `recordPage` fact the data plugin's index answers for a site: its base and
108
+ * the address field its pages are reached by. `null` where there is none, or
109
+ * where its address field is no longer one of the dataset's fields.
110
+ */ export function aiInventoryRecordPage(fact, fields) {
111
+ if (!fact || typeof fact !== 'object') return null;
112
+ const base = text(fact['base']).replace(/^\/+|\/+$/g, '');
113
+ const slugField = text(fact['slugField']);
114
+ if (!RECORD_BASE.test(base) || !slugField || !fields.some((field)=>field.id === slugField)) return null;
115
+ return {
116
+ recordBase: base,
117
+ addressField: slugField
118
+ };
119
+ }
104
120
  /**
105
121
  * The org's datasets shared with the site, through the `dataset` index the
106
122
  * data plugin publishes (AGL-3080) rather than its collection: their names
@@ -119,13 +135,21 @@ async function readWindow(query, fields, cap, map) {
119
135
  });
120
136
  return {
121
137
  rows: records.map((record)=>{
138
+ var _ref;
139
+ var _fields_find;
122
140
  const fields = (Array.isArray(record.facts['fields']) ? record.facts['fields'] : []).map((field)=>({
123
141
  id: text(field == null ? void 0 : field['id']),
124
142
  name: text(field == null ? void 0 : field['name']) || text(field == null ? void 0 : field['id']),
125
- image: text(field == null ? void 0 : field['customType']) === 'image'
143
+ image: text(field == null ? void 0 : field['customType']) === 'image',
144
+ // A record page's address, the data plugin's `pageAddress` type (AGL-3727).
145
+ address: text(field == null ? void 0 : field['customType']) === 'pageAddress'
126
146
  })).filter((field)=>field.name);
127
147
  // A record's photo (AGL-3616), the data plugin's `image` field type.
128
148
  const image = fields.find((field)=>field.image && field.id);
149
+ // The record page the site already serves for it (AGL-3747), whose
150
+ // address field is the one its pages are reached by.
151
+ const page = aiInventoryRecordPage(record.facts['recordPage'], fields);
152
+ const address = (_ref = page == null ? void 0 : page.addressField) != null ? _ref : (_fields_find = fields.find((field)=>field.address && field.id)) == null ? void 0 : _fields_find.id;
129
153
  return _extends({
130
154
  id: record.id,
131
155
  name: record.name,
@@ -133,6 +157,10 @@ async function readWindow(query, fields, cap, map) {
133
157
  fieldIds: fields.map((field)=>field.id || field.name)
134
158
  }, image ? {
135
159
  imageField: image.id
160
+ } : {}, address ? {
161
+ addressField: address
162
+ } : {}, page ? {
163
+ recordBase: page.recordBase
136
164
  } : {});
137
165
  }),
138
166
  truncated
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/site-inventory.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * The screen a site's starter home may be (AGL-3594, AGL-3408): the one\n * `host.defaultHomeScreenId` names, while it still answers `/` in the routing\n * map and is listed as a page. `null` on every site whose owner published a\n * home page of their own, which clears the marker (AGL-3478), and on every\n * site created before the marker existed, whose home page is the owner's.\n */\nexport function aiStarterHomeCandidate(\n host: Readonly<Record<string, unknown>> | null,\n screens: ReadonlyArray<Pick<AiInventoryScreen, 'id' | 'template'>>,\n): string | null {\n const id = text(host?.['defaultHomeScreenId'])\n if (!id) return null\n const routing = host?.['screens']\n const path =\n routing && typeof routing === 'object' ? (routing as Record<string, unknown>)[id] : undefined\n if (path !== SCREEN_ROOT_PATH) return null\n return screens.some((screen) => screen.id === id && !screen.template) ? id : null\n}\n\n/**\n * Whether the starter home is as the platform wrote it: its one version is\n * the one it was provisioned and published with. A version saved in the\n * editor is an edit, published or not, and an edited starter is the owner's\n * page — a plan keeps it.\n */\nexport function aiStarterHomeUntouched(\n publishedVersionId: string,\n versionIds: readonly string[],\n): boolean {\n return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId\n}\n\n/**\n * The org's datasets shared with the site, through the `dataset` index the\n * data plugin publishes (AGL-3080) rather than its collection: their names\n * and their fields' names, in the dataset page's order. None where no plugin\n * keeps datasets here.\n */\nasync function readIndexedDatasets(\n orgId: string,\n hostId: string,\n cap: number,\n): Promise<Window<AiInventoryDataset>> {\n const datasets = pluginRecordIndex('dataset')\n if (!datasets) return { rows: [], truncated: false }\n const { records, truncated } = await datasets.index.list({ orgId, hostId, limit: cap })\n return {\n rows: records.map((record) => {\n const fields = (Array.isArray(record.facts['fields']) ? (record.facts['fields'] as Data[]) : [])\n .map((field) => ({\n id: text(field?.['id']),\n name: text(field?.['name']) || text(field?.['id']),\n image: text(field?.['customType']) === 'image',\n }))\n .filter((field) => field.name)\n // A record's photo (AGL-3616), the data plugin's `image` field type.\n const image = fields.find((field) => field.image && field.id)\n return {\n id: record.id,\n name: record.name,\n fields: fields.map((field) => field.name),\n fieldIds: fields.map((field) => field.id || field.name),\n ...(image ? { imageField: image.id } : {}),\n }\n }),\n truncated,\n }\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n // Each listed screen's published version, read in the same window, for the\n // starter home's untouched test below.\n const screenVersions = new Map<string, string>()\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readIndexedDatasets(orgId, hostId, cap),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt', 'versionId'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n screenVersions.set(id, text(data['versionId']))\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const hostData = hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null\n const starter = aiStarterHomeCandidate(hostData, screens.rows)\n if (starter) {\n // One more read, and only on a site that still carries the marker: the\n // starter's versions, of which an untouched one has exactly the one it\n // was provisioned with.\n const versions = await host\n .collection('screens')\n .doc(starter)\n .collection('versions')\n .select()\n .limit(2)\n .get()\n if (aiStarterHomeUntouched(screenVersions.get(starter) ?? '', versions.docs.map((doc) => doc.id))) {\n const row = screens.rows.find((screen) => screen.id === starter)\n if (row) row.replaceable = true\n }\n }\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostData),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","SCREEN_ROOT_PATH","pluginRecordIndex","firebaseAdmin","resolveOrgIdForHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","aiStarterHomeCandidate","screens","routing","undefined","some","screen","template","aiStarterHomeUntouched","publishedVersionId","versionIds","Boolean","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","facts","name","image","filter","find","fieldIds","imageField","readSiteInventory","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","screenVersions","Map","components","layouts","templates","forms","collections","Promise","all","props","bracketedDefaults","prop","keys","parentId","kind","slug","set","layoutId","hostData","exists","starter","versions","replaceable","windows"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/site-inventory.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n describeTheme,\n resolveSiteTheme,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport {\n REUSABLE_COMPONENT_KIND_EMAIL,\n reusableComponentKindOf,\n} from '@aglyn/aglyn/app-utils/reusable-component-kind'\nimport { SCREEN_KIND_TEMPLATE, SCREEN_ROOT_PATH } from '@aglyn/aglyn/app-utils/screen-route'\nimport { pluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport {\n AI_INVENTORY_KINDS,\n AI_SITE_INVENTORY_MAX_PER_KIND,\n AiInventoryScopeError,\n type AiComponentPropTypes,\n type AiInventoryCollection,\n type AiInventoryComponent,\n type AiInventoryDataset,\n type AiInventoryForm,\n type AiInventoryKind,\n type AiInventoryLayout,\n type AiInventoryScreen,\n type AiInventoryTemplate,\n type AiInventoryTheme,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { aiBracketedFacts } from './ai-doctrine-validators'\n\n/**\n * The reader behind the site inventory (AGL-2935): one site's components,\n * layouts, templates, forms, datasets, content collections, screens and\n * theme, read as projections through the Admin SDK and reduced to the\n * compact shape `model/ai-site-inventory.ts` describes.\n *\n * Every kind is a projection (`select`) of the fields the inventory names —\n * never a node map, never a record's content — read one row past the cap so\n * a full page is told apart from a cut one. Retired rows (soft-deleted,\n * archived, a component never published) are filtered after the read, so a\n * site with many retired rows can list fewer than the cap; the kind is still\n * marked truncated when the read was cut, which is the honest answer.\n *\n * The window is WIDER than the prompt lists (AGL-2937). The prompt block\n * lists `AI_SITE_INVENTORY_LISTED_PER_KIND` records of a kind because every\n * character of it is billed on every attempt; the rest of the window is what\n * the lookup tool searches, and a record never read cannot be found. The\n * difference is paid in document reads rather than tokens — a projection of\n * a few fields, once per job step — which is the cheaper of the two by some\n * orders of magnitude.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nexport interface ReadSiteInventoryOptions {\n /** The Admin SDK handle; the platform's default app when absent. */\n firestore?: Firestore\n /** Rows read per kind; `AI_SITE_INVENTORY_MAX_PER_KIND` when absent, and never above it. */\n maxPerKind?: number\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/** A record's name, or its id when it was saved without one. */\nfunction nameOf(data: Data, id: string, ...fields: string[]): string {\n for (const field of fields) {\n const value = text(data[field])\n if (value) return value\n }\n return id\n}\n\n/** The flat `path → color` leaves of one scheme, e.g. `primary.main`. */\nfunction schemeColors(scheme: unknown): Record<string, string> {\n const colors: Record<string, string> = {}\n const visit = (value: unknown, path: string) => {\n if (typeof value === 'string') {\n if (path) colors[path] = value\n return\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) return\n for (const [key, inner] of Object.entries(value as Data)) {\n visit(inner, path ? `${path}.${key}` : key)\n }\n }\n visit(scheme, '')\n return colors\n}\n\n/** The theme a site renders with, reduced to what a plan and an email can act on. */\nexport function aiInventoryTheme(host: Data | null): AiInventoryTheme | null {\n const theme = resolveSiteTheme(host as ThemeHostDocument | null)\n if (!theme) return null\n const fonts = new Set<string>()\n for (const font of theme.fonts ?? []) {\n if (text(font?.family)) fonts.add(text(font.family))\n }\n return {\n summary: describeTheme(theme),\n colors: schemeColors(theme.colorSchemes?.light),\n fonts: [...fonts],\n }\n}\n\ninterface Window<T> {\n rows: T[]\n truncated: boolean\n}\n\nasync function readWindow<T>(\n query: FirebaseFirestore.Query,\n fields: string[],\n cap: number,\n map: (id: string, data: Data) => T | null,\n): Promise<Window<T>> {\n const snapshot = await query\n .select(...fields)\n .limit(cap + 1)\n .get()\n const rows: T[] = []\n for (const doc of snapshot.docs.slice(0, cap)) {\n const row = map(doc.id, (doc.data() ?? {}) as Data)\n if (row) rows.push(row)\n }\n return { rows, truncated: snapshot.docs.length > cap }\n}\n\n/**\n * The screen a site's starter home may be (AGL-3594, AGL-3408): the one\n * `host.defaultHomeScreenId` names, while it still answers `/` in the routing\n * map and is listed as a page. `null` on every site whose owner published a\n * home page of their own, which clears the marker (AGL-3478), and on every\n * site created before the marker existed, whose home page is the owner's.\n */\nexport function aiStarterHomeCandidate(\n host: Readonly<Record<string, unknown>> | null,\n screens: ReadonlyArray<Pick<AiInventoryScreen, 'id' | 'template'>>,\n): string | null {\n const id = text(host?.['defaultHomeScreenId'])\n if (!id) return null\n const routing = host?.['screens']\n const path =\n routing && typeof routing === 'object' ? (routing as Record<string, unknown>)[id] : undefined\n if (path !== SCREEN_ROOT_PATH) return null\n return screens.some((screen) => screen.id === id && !screen.template) ? id : null\n}\n\n/**\n * Whether the starter home is as the platform wrote it: its one version is\n * the one it was provisioned and published with. A version saved in the\n * editor is an edit, published or not, and an edited starter is the owner's\n * page — a plan keeps it.\n */\nexport function aiStarterHomeUntouched(\n publishedVersionId: string,\n versionIds: readonly string[],\n): boolean {\n return Boolean(publishedVersionId) && versionIds.length === 1 && versionIds[0] === publishedVersionId\n}\n\n/** A record template's base, as the data plugin keeps it: one to four URL-safe segments. */\nconst RECORD_BASE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\\/[a-z0-9]+(?:-[a-z0-9]+)*){0,3}$/\n\n/**\n * The record page a site already serves for a dataset (AGL-3747), from the\n * `recordPage` fact the data plugin's index answers for a site: its base and\n * the address field its pages are reached by. `null` where there is none, or\n * where its address field is no longer one of the dataset's fields.\n */\nexport function aiInventoryRecordPage(\n fact: unknown,\n fields: ReadonlyArray<{ id: string }>,\n): { recordBase: string; addressField: string } | null {\n if (!fact || typeof fact !== 'object') return null\n const base = text((fact as Data)['base']).replace(/^\\/+|\\/+$/g, '')\n const slugField = text((fact as Data)['slugField'])\n if (!RECORD_BASE.test(base) || !slugField || !fields.some((field) => field.id === slugField)) return null\n return { recordBase: base, addressField: slugField }\n}\n\n/**\n * The org's datasets shared with the site, through the `dataset` index the\n * data plugin publishes (AGL-3080) rather than its collection: their names\n * and their fields' names, in the dataset page's order. None where no plugin\n * keeps datasets here.\n */\nasync function readIndexedDatasets(\n orgId: string,\n hostId: string,\n cap: number,\n): Promise<Window<AiInventoryDataset>> {\n const datasets = pluginRecordIndex('dataset')\n if (!datasets) return { rows: [], truncated: false }\n const { records, truncated } = await datasets.index.list({ orgId, hostId, limit: cap })\n return {\n rows: records.map((record) => {\n const fields = (Array.isArray(record.facts['fields']) ? (record.facts['fields'] as Data[]) : [])\n .map((field) => ({\n id: text(field?.['id']),\n name: text(field?.['name']) || text(field?.['id']),\n image: text(field?.['customType']) === 'image',\n // A record page's address, the data plugin's `pageAddress` type (AGL-3727).\n address: text(field?.['customType']) === 'pageAddress',\n }))\n .filter((field) => field.name)\n // A record's photo (AGL-3616), the data plugin's `image` field type.\n const image = fields.find((field) => field.image && field.id)\n // The record page the site already serves for it (AGL-3747), whose\n // address field is the one its pages are reached by.\n const page = aiInventoryRecordPage(record.facts['recordPage'], fields)\n const address = page?.addressField ?? fields.find((field) => field.address && field.id)?.id\n return {\n id: record.id,\n name: record.name,\n fields: fields.map((field) => field.name),\n fieldIds: fields.map((field) => field.id || field.name),\n ...(image ? { imageField: image.id } : {}),\n ...(address ? { addressField: address } : {}),\n ...(page ? { recordBase: page.recordBase } : {}),\n }\n }),\n truncated,\n }\n}\n\n/**\n * Read one site's inventory for a generation.\n *\n * SCOPED before anything is read. The job names a site and an org, and\n * nothing the reader is handed proves the site belongs to the org, so it asks\n * the host index first and refuses a pairing it does not record: another\n * workspace's component names are not this workspace's prompt. Datasets are\n * org-owned and narrowed to what this site may see.\n *\n * Throws `AiInventoryScopeError` for a site outside the org, and lets a\n * Firestore failure propagate: a planner handed an empty inventory by\n * mistake would create everything the site already has.\n */\nexport async function readSiteInventory(\n orgId: string,\n hostId: string,\n options: ReadSiteInventoryOptions = {},\n): Promise<AiSiteInventory> {\n const owner = await resolveOrgIdForHost(hostId)\n if (!owner || owner !== orgId) throw new AiInventoryScopeError(hostId)\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const cap = Math.min(\n AI_SITE_INVENTORY_MAX_PER_KIND,\n Math.max(1, Math.floor(options.maxPerKind ?? AI_SITE_INVENTORY_MAX_PER_KIND)),\n )\n const host = firestore.collection('hosts').doc(hostId)\n // Each listed screen's published version, read in the same window, for the\n // starter home's untouched test below.\n const screenVersions = new Map<string, string>()\n\n const [components, layouts, templates, forms, datasets, collections, screens, hostDoc] =\n await Promise.all([\n readWindow<AiInventoryComponent>(\n host.collection('components'),\n ['displayName', 'props', 'rootId', 'versionId', 'deletedAt', 'kind'],\n cap,\n (id, data) => {\n // Soft-deleted, or never published: an instance of either renders nothing.\n if (data['deletedAt'] || !(data['rootId'] || data['versionId'])) return null\n // An email block (AGL-3287) is built from email elements and placed\n // in emails alone: no page, layout or component a job builds can\n // render one, and the email a job builds places no instances.\n if (reusableComponentKindOf(data) === REUSABLE_COMPONENT_KIND_EMAIL) return null\n const props: AiComponentPropTypes = {}\n const bracketedDefaults: Record<string, string[]> = {}\n for (const prop of Array.isArray(data['props']) ? (data['props'] as Data[]) : []) {\n const name = text(prop?.['name'])\n if (!name) continue\n props[name] = text(prop['type']) || 'text'\n const facts = aiBracketedFacts([text(prop['defaultValue'])])\n if (facts.length) bracketedDefaults[name] = facts\n }\n return {\n id,\n name: nameOf(data, id, 'displayName'),\n props,\n ...(Object.keys(bracketedDefaults).length ? { bracketedDefaults } : {}),\n }\n },\n ),\n readWindow<AiInventoryLayout>(\n host.collection('layouts'),\n ['displayName', 'layoutId', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName'),\n parentId: text(data['layoutId']) || null,\n },\n ),\n readWindow<AiInventoryTemplate>(\n host.collection('templates'),\n ['displayName', 'kind', 'deletedAt'],\n cap,\n (id, data) =>\n data['deletedAt']\n ? null\n : { id, name: nameOf(data, id, 'displayName'), kind: text(data['kind']) || 'page' },\n ),\n readWindow<AiInventoryForm>(\n host.collection('forms'),\n ['displayName', 'slug', 'fields', 'archivedAt'],\n cap,\n (id, data) =>\n isFormArchived(data as { archivedAt?: unknown })\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n },\n ),\n readIndexedDatasets(orgId, hostId, cap),\n readWindow<AiInventoryCollection>(\n host.collection('collections'),\n ['displayName', 'name', 'slug', 'kind'],\n cap,\n (id, data) =>\n hostCollectionKind(data) !== 'content'\n ? null\n : {\n id,\n name: nameOf(data, id, 'displayName', 'name', 'slug'),\n slug: text(data['slug']),\n },\n ),\n readWindow<AiInventoryScreen>(\n host.collection('screens'),\n ['displayName', 'slug', 'layoutId', 'kind', 'deletedAt', 'versionId'],\n cap,\n (id, data) => {\n const kind = text(data['kind'])\n // An email design and an error body are not pages a plan links or duplicates.\n if (data['deletedAt'] || (kind && kind !== SCREEN_KIND_TEMPLATE)) return null\n screenVersions.set(id, text(data['versionId']))\n return {\n id,\n name: nameOf(data, id, 'displayName', 'slug'),\n slug: text(data['slug']),\n layoutId: text(data['layoutId']) || null,\n template: kind === SCREEN_KIND_TEMPLATE,\n }\n },\n ),\n host.get(),\n ])\n\n const hostData = hostDoc.exists ? ((hostDoc.data() ?? {}) as Data) : null\n const starter = aiStarterHomeCandidate(hostData, screens.rows)\n if (starter) {\n // One more read, and only on a site that still carries the marker: the\n // starter's versions, of which an untouched one has exactly the one it\n // was provisioned with.\n const versions = await host\n .collection('screens')\n .doc(starter)\n .collection('versions')\n .select()\n .limit(2)\n .get()\n if (aiStarterHomeUntouched(screenVersions.get(starter) ?? '', versions.docs.map((doc) => doc.id))) {\n const row = screens.rows.find((screen) => screen.id === starter)\n if (row) row.replaceable = true\n }\n }\n\n const windows: Record<AiInventoryKind, Window<unknown>> = {\n components,\n layouts,\n templates,\n forms,\n datasets,\n collections,\n screens,\n }\n return {\n hostId,\n components: components.rows,\n layouts: layouts.rows,\n templates: templates.rows,\n forms: forms.rows,\n datasets: datasets.rows,\n collections: collections.rows,\n screens: screens.rows,\n theme: aiInventoryTheme(hostData),\n truncated: AI_INVENTORY_KINDS.filter((kind) => windows[kind].truncated),\n }\n}\n"],"names":["hostCollectionKind","isFormArchived","describeTheme","resolveSiteTheme","REUSABLE_COMPONENT_KIND_EMAIL","reusableComponentKindOf","SCREEN_KIND_TEMPLATE","SCREEN_ROOT_PATH","pluginRecordIndex","firebaseAdmin","resolveOrgIdForHost","AI_INVENTORY_KINDS","AI_SITE_INVENTORY_MAX_PER_KIND","AiInventoryScopeError","aiBracketedFacts","text","value","trim","nameOf","data","id","fields","field","schemeColors","scheme","colors","visit","path","Array","isArray","key","inner","Object","entries","aiInventoryTheme","host","theme","fonts","Set","font","family","add","summary","colorSchemes","light","readWindow","query","cap","map","snapshot","select","limit","get","rows","doc","docs","slice","row","push","truncated","length","aiStarterHomeCandidate","screens","routing","undefined","some","screen","template","aiStarterHomeUntouched","publishedVersionId","versionIds","Boolean","RECORD_BASE","aiInventoryRecordPage","fact","base","replace","slugField","test","recordBase","addressField","readIndexedDatasets","orgId","hostId","datasets","records","index","list","record","facts","name","image","address","filter","find","page","fieldIds","imageField","readSiteInventory","options","hostDoc","owner","firestore","app","Math","min","max","floor","maxPerKind","collection","screenVersions","Map","components","layouts","templates","forms","collections","Promise","all","props","bracketedDefaults","prop","keys","parentId","kind","slug","set","layoutId","hostData","exists","starter","versions","replaceable","windows"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,yCAAwC;AAC3E,SAASC,cAAc,QAAQ,+BAA8B;AAC7D,SACEC,aAAa,EACbC,gBAAgB,QAEX,oCAAmC;AAC1C,SACEC,6BAA6B,EAC7BC,uBAAuB,QAClB,iDAAgD;AACvD,SAASC,oBAAoB,EAAEC,gBAAgB,QAAQ,sCAAqC;AAC5F,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,mBAAmB,QAAQ,gDAA+C;AACnF,SACEC,kBAAkB,EAClBC,8BAA8B,EAC9BC,qBAAqB,QAYhB,gCAA4B;AACnC,SAASC,gBAAgB,QAAQ,8BAA0B;AAkC3D,SAASC,KAAKC,KAAc;IAC1B,OAAO,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AACpD;AAEA,8DAA8D,GAC9D,SAASC,OAAOC,IAAU,EAAEC,EAAU,EAAE,GAAGC,MAAgB;IACzD,KAAK,MAAMC,SAASD,OAAQ;QAC1B,MAAML,QAAQD,KAAKI,IAAI,CAACG,MAAM;QAC9B,IAAIN,OAAO,OAAOA;IACpB;IACA,OAAOI;AACT;AAEA,uEAAuE,GACvE,SAASG,aAAaC,MAAe;IACnC,MAAMC,SAAiC,CAAC;IACxC,MAAMC,QAAQ,CAACV,OAAgBW;QAC7B,IAAI,OAAOX,UAAU,UAAU;YAC7B,IAAIW,MAAMF,MAAM,CAACE,KAAK,GAAGX;YACzB;QACF;QACA,IAAI,CAACA,SAAS,OAAOA,UAAU,YAAYY,MAAMC,OAAO,CAACb,QAAQ;QACjE,KAAK,MAAM,CAACc,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACjB,OAAgB;YACxDU,MAAMK,OAAOJ,OAAO,GAAGA,KAAK,CAAC,EAAEG,KAAK,GAAGA;QACzC;IACF;IACAJ,MAAMF,QAAQ;IACd,OAAOC;AACT;AAEA,mFAAmF,GACnF,OAAO,SAASS,iBAAiBC,IAAiB;QAI7BC;QAKIA;IARvB,MAAMA,QAAQjC,iBAAiBgC;IAC/B,IAAI,CAACC,OAAO,OAAO;IACnB,MAAMC,QAAQ,IAAIC;IAClB,KAAK,MAAMC,SAAQH,eAAAA,MAAMC,KAAK,YAAXD,eAAe,EAAE,CAAE;QACpC,IAAIrB,KAAKwB,wBAAAA,KAAMC,MAAM,GAAGH,MAAMI,GAAG,CAAC1B,KAAKwB,KAAKC,MAAM;IACpD;IACA,OAAO;QACLE,SAASxC,cAAckC;QACvBX,QAAQF,cAAaa,sBAAAA,MAAMO,YAAY,qBAAlBP,oBAAoBQ,KAAK;QAC9CP,OAAO;eAAIA;SAAM;IACnB;AACF;AAOA,eAAeQ,WACbC,KAA8B,EAC9BzB,MAAgB,EAChB0B,GAAW,EACXC,GAAyC;IAEzC,MAAMC,WAAW,MAAMH,MACpBI,MAAM,IAAI7B,QACV8B,KAAK,CAACJ,MAAM,GACZK,GAAG;IACN,MAAMC,OAAY,EAAE;IACpB,KAAK,MAAMC,OAAOL,SAASM,IAAI,CAACC,KAAK,CAAC,GAAGT,KAAM;YACpBO;QAAzB,MAAMG,MAAMT,IAAIM,IAAIlC,EAAE,GAAGkC,YAAAA,IAAInC,IAAI,cAARmC,YAAc,CAAC;QACxC,IAAIG,KAAKJ,KAAKK,IAAI,CAACD;IACrB;IACA,OAAO;QAAEJ;QAAMM,WAAWV,SAASM,IAAI,CAACK,MAAM,GAAGb;IAAI;AACvD;AAEA;;;;;;CAMC,GACD,OAAO,SAASc,uBACd1B,IAA8C,EAC9C2B,OAAkE;IAElE,MAAM1C,KAAKL,KAAKoB,wBAAAA,IAAM,CAAC,sBAAsB;IAC7C,IAAI,CAACf,IAAI,OAAO;IAChB,MAAM2C,UAAU5B,wBAAAA,IAAM,CAAC,UAAU;IACjC,MAAMR,OACJoC,WAAW,OAAOA,YAAY,WAAW,AAACA,OAAmC,CAAC3C,GAAG,GAAG4C;IACtF,IAAIrC,SAASpB,kBAAkB,OAAO;IACtC,OAAOuD,QAAQG,IAAI,CAAC,CAACC,SAAWA,OAAO9C,EAAE,KAAKA,MAAM,CAAC8C,OAAOC,QAAQ,IAAI/C,KAAK;AAC/E;AAEA;;;;;CAKC,GACD,OAAO,SAASgD,uBACdC,kBAA0B,EAC1BC,UAA6B;IAE7B,OAAOC,QAAQF,uBAAuBC,WAAWV,MAAM,KAAK,KAAKU,UAAU,CAAC,EAAE,KAAKD;AACrF;AAEA,0FAA0F,GAC1F,MAAMG,cAAc;AAEpB;;;;;CAKC,GACD,OAAO,SAASC,sBACdC,IAAa,EACbrD,MAAqC;IAErC,IAAI,CAACqD,QAAQ,OAAOA,SAAS,UAAU,OAAO;IAC9C,MAAMC,OAAO5D,KAAK,AAAC2D,IAAa,CAAC,OAAO,EAAEE,OAAO,CAAC,cAAc;IAChE,MAAMC,YAAY9D,KAAK,AAAC2D,IAAa,CAAC,YAAY;IAClD,IAAI,CAACF,YAAYM,IAAI,CAACH,SAAS,CAACE,aAAa,CAACxD,OAAO4C,IAAI,CAAC,CAAC3C,QAAUA,MAAMF,EAAE,KAAKyD,YAAY,OAAO;IACrG,OAAO;QAAEE,YAAYJ;QAAMK,cAAcH;IAAU;AACrD;AAEA;;;;;CAKC,GACD,eAAeI,oBACbC,KAAa,EACbC,MAAc,EACdpC,GAAW;IAEX,MAAMqC,WAAW5E,kBAAkB;IACnC,IAAI,CAAC4E,UAAU,OAAO;QAAE/B,MAAM,EAAE;QAAEM,WAAW;IAAM;IACnD,MAAM,EAAE0B,OAAO,EAAE1B,SAAS,EAAE,GAAG,MAAMyB,SAASE,KAAK,CAACC,IAAI,CAAC;QAAEL;QAAOC;QAAQhC,OAAOJ;IAAI;IACrF,OAAO;QACLM,MAAMgC,QAAQrC,GAAG,CAAC,CAACwC;;gBAeqBnE;YAdtC,MAAMA,SAAS,AAACO,CAAAA,MAAMC,OAAO,CAAC2D,OAAOC,KAAK,CAAC,SAAS,IAAKD,OAAOC,KAAK,CAAC,SAAS,GAAc,EAAE,AAAD,EAC3FzC,GAAG,CAAC,CAAC1B,QAAW,CAAA;oBACfF,IAAIL,KAAKO,yBAAAA,KAAO,CAAC,KAAK;oBACtBoE,MAAM3E,KAAKO,yBAAAA,KAAO,CAAC,OAAO,KAAKP,KAAKO,yBAAAA,KAAO,CAAC,KAAK;oBACjDqE,OAAO5E,KAAKO,yBAAAA,KAAO,CAAC,aAAa,MAAM;oBACvC,4EAA4E;oBAC5EsE,SAAS7E,KAAKO,yBAAAA,KAAO,CAAC,aAAa,MAAM;gBAC3C,CAAA,GACCuE,MAAM,CAAC,CAACvE,QAAUA,MAAMoE,IAAI;YAC/B,qEAAqE;YACrE,MAAMC,QAAQtE,OAAOyE,IAAI,CAAC,CAACxE,QAAUA,MAAMqE,KAAK,IAAIrE,MAAMF,EAAE;YAC5D,mEAAmE;YACnE,qDAAqD;YACrD,MAAM2E,OAAOtB,sBAAsBe,OAAOC,KAAK,CAAC,aAAa,EAAEpE;YAC/D,MAAMuE,kBAAUG,wBAAAA,KAAMf,YAAY,oBAAI3D,eAAAA,OAAOyE,IAAI,CAAC,CAACxE,QAAUA,MAAMsE,OAAO,IAAItE,MAAMF,EAAE,sBAAhDC,aAAmDD,EAAE;YAC3F,OAAO;gBACLA,IAAIoE,OAAOpE,EAAE;gBACbsE,MAAMF,OAAOE,IAAI;gBACjBrE,QAAQA,OAAO2B,GAAG,CAAC,CAAC1B,QAAUA,MAAMoE,IAAI;gBACxCM,UAAU3E,OAAO2B,GAAG,CAAC,CAAC1B,QAAUA,MAAMF,EAAE,IAAIE,MAAMoE,IAAI;eAClDC,QAAQ;gBAAEM,YAAYN,MAAMvE,EAAE;YAAC,IAAI,CAAC,GACpCwE,UAAU;gBAAEZ,cAAcY;YAAQ,IAAI,CAAC,GACvCG,OAAO;gBAAEhB,YAAYgB,KAAKhB,UAAU;YAAC,IAAI,CAAC;QAElD;QACApB;IACF;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeuC,kBACpBhB,KAAa,EACbC,MAAc,EACdgB,UAAoC,CAAC,CAAC;QAIpBA,oBAGOA,qBA6GWC;IAlHpC,MAAMC,QAAQ,MAAM3F,oBAAoByE;IACxC,IAAI,CAACkB,SAASA,UAAUnB,OAAO,MAAM,IAAIrE,sBAAsBsE;IAC/D,MAAMmB,aAAYH,qBAAAA,QAAQG,SAAS,YAAjBH,qBAAqB1F,cAAc8F,GAAG,GAAGD,SAAS;IACpE,MAAMvD,MAAMyD,KAAKC,GAAG,CAClB7F,gCACA4F,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,EAACR,sBAAAA,QAAQS,UAAU,YAAlBT,sBAAsBvF;IAE/C,MAAMuB,OAAOmE,UAAUO,UAAU,CAAC,SAASvD,GAAG,CAAC6B;IAC/C,2EAA2E;IAC3E,uCAAuC;IACvC,MAAM2B,iBAAiB,IAAIC;IAE3B,MAAM,CAACC,YAAYC,SAASC,WAAWC,OAAO/B,UAAUgC,aAAatD,SAASsC,QAAQ,GACpF,MAAMiB,QAAQC,GAAG,CAAC;QAChBzE,WACEV,KAAK0E,UAAU,CAAC,eAChB;YAAC;YAAe;YAAS;YAAU;YAAa;YAAa;SAAO,EACpE9D,KACA,CAAC3B,IAAID;YACH,2EAA2E;YAC3E,IAAIA,IAAI,CAAC,YAAY,IAAI,CAAEA,CAAAA,IAAI,CAAC,SAAS,IAAIA,IAAI,CAAC,YAAY,AAAD,GAAI,OAAO;YACxE,oEAAoE;YACpE,iEAAiE;YACjE,8DAA8D;YAC9D,IAAId,wBAAwBc,UAAUf,+BAA+B,OAAO;YAC5E,MAAMmH,QAA8B,CAAC;YACrC,MAAMC,oBAA8C,CAAC;YACrD,KAAK,MAAMC,QAAQ7F,MAAMC,OAAO,CAACV,IAAI,CAAC,QAAQ,IAAKA,IAAI,CAAC,QAAQ,GAAc,EAAE,CAAE;gBAChF,MAAMuE,OAAO3E,KAAK0G,wBAAAA,IAAM,CAAC,OAAO;gBAChC,IAAI,CAAC/B,MAAM;gBACX6B,KAAK,CAAC7B,KAAK,GAAG3E,KAAK0G,IAAI,CAAC,OAAO,KAAK;gBACpC,MAAMhC,QAAQ3E,iBAAiB;oBAACC,KAAK0G,IAAI,CAAC,eAAe;iBAAE;gBAC3D,IAAIhC,MAAM7B,MAAM,EAAE4D,iBAAiB,CAAC9B,KAAK,GAAGD;YAC9C;YACA,OAAO;gBACLrE;gBACAsE,MAAMxE,OAAOC,MAAMC,IAAI;gBACvBmG;eACIvF,OAAO0F,IAAI,CAACF,mBAAmB5D,MAAM,GAAG;gBAAE4D;YAAkB,IAAI,CAAC;QAEzE;QAEF3E,WACEV,KAAK0E,UAAU,CAAC,YAChB;YAAC;YAAe;YAAY;SAAY,EACxC9D,KACA,CAAC3B,IAAID,OACHA,IAAI,CAAC,YAAY,GACb,OACA;gBACEC;gBACAsE,MAAMxE,OAAOC,MAAMC,IAAI;gBACvBuG,UAAU5G,KAAKI,IAAI,CAAC,WAAW,KAAK;YACtC;QAER0B,WACEV,KAAK0E,UAAU,CAAC,cAChB;YAAC;YAAe;YAAQ;SAAY,EACpC9D,KACA,CAAC3B,IAAID,OACHA,IAAI,CAAC,YAAY,GACb,OACA;gBAAEC;gBAAIsE,MAAMxE,OAAOC,MAAMC,IAAI;gBAAgBwG,MAAM7G,KAAKI,IAAI,CAAC,OAAO,KAAK;YAAO;QAExF0B,WACEV,KAAK0E,UAAU,CAAC,UAChB;YAAC;YAAe;YAAQ;YAAU;SAAa,EAC/C9D,KACA,CAAC3B,IAAID,OACHlB,eAAekB,QACX,OACA;gBACEC;gBACAsE,MAAMxE,OAAOC,MAAMC,IAAI,eAAe;gBACtCC,QAAQ,AAACO,CAAAA,MAAMC,OAAO,CAACV,IAAI,CAAC,SAAS,IAAKA,IAAI,CAAC,SAAS,GAAc,EAAE,AAAD,EACpE6B,GAAG,CAAC,CAAC1B,QAAUP,KAAKO,yBAAAA,KAAO,CAAC,YAAY,GACxCuE,MAAM,CAACtB;YACZ;QAERU,oBAAoBC,OAAOC,QAAQpC;QACnCF,WACEV,KAAK0E,UAAU,CAAC,gBAChB;YAAC;YAAe;YAAQ;YAAQ;SAAO,EACvC9D,KACA,CAAC3B,IAAID,OACHnB,mBAAmBmB,UAAU,YACzB,OACA;gBACEC;gBACAsE,MAAMxE,OAAOC,MAAMC,IAAI,eAAe,QAAQ;gBAC9CyG,MAAM9G,KAAKI,IAAI,CAAC,OAAO;YACzB;QAER0B,WACEV,KAAK0E,UAAU,CAAC,YAChB;YAAC;YAAe;YAAQ;YAAY;YAAQ;YAAa;SAAY,EACrE9D,KACA,CAAC3B,IAAID;YACH,MAAMyG,OAAO7G,KAAKI,IAAI,CAAC,OAAO;YAC9B,8EAA8E;YAC9E,IAAIA,IAAI,CAAC,YAAY,IAAKyG,QAAQA,SAAStH,sBAAuB,OAAO;YACzEwG,eAAegB,GAAG,CAAC1G,IAAIL,KAAKI,IAAI,CAAC,YAAY;YAC7C,OAAO;gBACLC;gBACAsE,MAAMxE,OAAOC,MAAMC,IAAI,eAAe;gBACtCyG,MAAM9G,KAAKI,IAAI,CAAC,OAAO;gBACvB4G,UAAUhH,KAAKI,IAAI,CAAC,WAAW,KAAK;gBACpCgD,UAAUyD,SAAStH;YACrB;QACF;QAEF6B,KAAKiB,GAAG;KACT;IAEH,MAAM4E,WAAW5B,QAAQ6B,MAAM,IAAK7B,gBAAAA,QAAQjF,IAAI,cAAZiF,gBAAkB,CAAC,IAAc;IACrE,MAAM8B,UAAUrE,uBAAuBmE,UAAUlE,QAAQT,IAAI;IAC7D,IAAI6E,SAAS;YAWgBpB;QAV3B,uEAAuE;QACvE,uEAAuE;QACvE,wBAAwB;QACxB,MAAMqB,WAAW,MAAMhG,KACpB0E,UAAU,CAAC,WACXvD,GAAG,CAAC4E,SACJrB,UAAU,CAAC,YACX3D,MAAM,GACNC,KAAK,CAAC,GACNC,GAAG;QACN,IAAIgB,wBAAuB0C,sBAAAA,eAAe1D,GAAG,CAAC8E,oBAAnBpB,sBAA+B,IAAIqB,SAAS5E,IAAI,CAACP,GAAG,CAAC,CAACM,MAAQA,IAAIlC,EAAE,IAAI;YACjG,MAAMqC,MAAMK,QAAQT,IAAI,CAACyC,IAAI,CAAC,CAAC5B,SAAWA,OAAO9C,EAAE,KAAK8G;YACxD,IAAIzE,KAAKA,IAAI2E,WAAW,GAAG;QAC7B;IACF;IAEA,MAAMC,UAAoD;QACxDrB;QACAC;QACAC;QACAC;QACA/B;QACAgC;QACAtD;IACF;IACA,OAAO;QACLqB;QACA6B,YAAYA,WAAW3D,IAAI;QAC3B4D,SAASA,QAAQ5D,IAAI;QACrB6D,WAAWA,UAAU7D,IAAI;QACzB8D,OAAOA,MAAM9D,IAAI;QACjB+B,UAAUA,SAAS/B,IAAI;QACvB+D,aAAaA,YAAY/D,IAAI;QAC7BS,SAASA,QAAQT,IAAI;QACrBjB,OAAOF,iBAAiB8F;QACxBrE,WAAWhD,mBAAmBkF,MAAM,CAAC,CAAC+B,OAASS,OAAO,CAACT,KAAK,CAACjE,SAAS;IACxE;AACF"}