@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.288

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 (228) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  3. package/src/lib/assets/site-styles/professional.webp +0 -0
  4. package/src/lib/assets/site-styles/store.webp +0 -0
  5. package/src/lib/assets/site-styles/trades.webp +0 -0
  6. package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
  7. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  8. package/src/lib/declarations.js +5 -2
  9. package/src/lib/declarations.js.map +1 -1
  10. package/src/lib/declarations.server.js +12 -0
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
  13. package/src/lib/jobs/ai-build-capabilities.js +27 -2
  14. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  15. package/src/lib/jobs/ai-business-profile-prefill.js +6 -3
  16. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
  17. package/src/lib/jobs/ai-image-reads.d.ts +59 -0
  18. package/src/lib/jobs/ai-image-reads.js +138 -0
  19. package/src/lib/jobs/ai-image-reads.js.map +1 -0
  20. package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
  21. package/src/lib/jobs/ai-job-build-step.js +83 -12
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
  25. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
  27. package/src/lib/jobs/ai-job-import-step.js +15 -11
  28. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
  30. package/src/lib/jobs/ai-job-insight-step.js +23 -5
  31. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-language.js +2 -1
  33. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  34. package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
  35. package/src/lib/jobs/ai-job-media-step.js +6 -8
  36. package/src/lib/jobs/ai-job-media-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
  38. package/src/lib/jobs/ai-job-navigation-step.js +7 -9
  39. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-page-language.js +7 -1
  41. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-sections.js +16 -3
  43. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  44. package/src/lib/jobs/ai-job-page-step.js +9 -1
  45. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
  47. package/src/lib/jobs/ai-job-plan-step.js +78 -13
  48. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
  50. package/src/lib/jobs/ai-job-privacy-step.js +14 -15
  51. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-products-step.js +5 -3
  53. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
  55. package/src/lib/jobs/ai-job-settings-step.js +7 -8
  56. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
  58. package/src/lib/jobs/ai-job-site-content.js +67 -8
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
  61. package/src/lib/jobs/ai-job-site-datasets.js +300 -30
  62. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-step.js +6 -3
  64. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
  66. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
  67. package/src/lib/jobs/ai-job-text-step.js +2 -1
  68. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  69. package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
  70. package/src/lib/jobs/ai-job-tracking-step.js +7 -8
  71. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
  73. package/src/lib/jobs/ai-jobs-beat.js +18 -8
  74. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  75. package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
  76. package/src/lib/jobs/ai-jobs-notify.js +136 -3
  77. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  78. package/src/lib/jobs/ai-layout-stock-photos.d.ts +155 -7
  79. package/src/lib/jobs/ai-layout-stock-photos.js +1517 -220
  80. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  81. package/src/lib/jobs/ai-product-image.d.ts +14 -0
  82. package/src/lib/jobs/ai-product-image.js +73 -21
  83. package/src/lib/jobs/ai-product-image.js.map +1 -1
  84. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
  85. package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
  86. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  87. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
  88. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
  89. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
  90. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
  91. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
  92. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
  93. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
  94. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
  95. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
  96. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.d.ts +120 -0
  97. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js +410 -0
  98. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js.map +1 -0
  99. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
  100. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
  101. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
  102. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
  103. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
  104. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
  105. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.d.ts +26 -0
  106. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js +714 -0
  107. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js.map +1 -0
  108. package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
  109. package/src/lib/layout-language/ai-layout-compiler.js +194 -18
  110. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  111. package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
  112. package/src/lib/layout-language/ai-layout-copy.js +383 -3
  113. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  114. package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
  115. package/src/lib/layout-language/ai-layout-design.js +31 -11
  116. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  117. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
  118. package/src/lib/layout-language/ai-layout-listings.js +9 -1
  119. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  120. package/src/lib/layout-language/ai-layout-pictures.d.ts +39 -1
  121. package/src/lib/layout-language/ai-layout-pictures.js +141 -4
  122. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  123. package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
  124. package/src/lib/layout-language/ai-layout-storefront.js +100 -20
  125. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
  126. package/src/lib/model/ai-build-job.d.ts +7 -0
  127. package/src/lib/model/ai-build-job.js +36 -7
  128. package/src/lib/model/ai-build-job.js.map +1 -1
  129. package/src/lib/model/ai-build-plan.d.ts +28 -4
  130. package/src/lib/model/ai-build-plan.js +49 -9
  131. package/src/lib/model/ai-build-plan.js.map +1 -1
  132. package/src/lib/model/ai-build-progress.js +3 -1
  133. package/src/lib/model/ai-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-job-notice.d.ts +8 -0
  135. package/src/lib/model/ai-job-notice.js +33 -0
  136. package/src/lib/model/ai-job-notice.js.map +1 -1
  137. package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
  138. package/src/lib/model/ai-job-plan-lines.js +6 -1
  139. package/src/lib/model/ai-job-plan-lines.js.map +1 -1
  140. package/src/lib/model/ai-jobs.types.d.ts +8 -0
  141. package/src/lib/model/ai-jobs.types.js.map +1 -1
  142. package/src/lib/model/ai-permissions.d.ts +13 -0
  143. package/src/lib/model/ai-permissions.js +29 -1
  144. package/src/lib/model/ai-permissions.js.map +1 -1
  145. package/src/lib/model/ai-plan-said-sections.d.ts +12 -3
  146. package/src/lib/model/ai-plan-said-sections.js +22 -5
  147. package/src/lib/model/ai-plan-said-sections.js.map +1 -1
  148. package/src/lib/model/ai-products.d.ts +18 -0
  149. package/src/lib/model/ai-products.js +32 -0
  150. package/src/lib/model/ai-products.js.map +1 -1
  151. package/src/lib/model/ai-site-context.d.ts +6 -2
  152. package/src/lib/model/ai-site-context.js +8 -2
  153. package/src/lib/model/ai-site-context.js.map +1 -1
  154. package/src/lib/model/ai-site-inspiration.d.ts +3 -2
  155. package/src/lib/model/ai-site-inspiration.js +3 -2
  156. package/src/lib/model/ai-site-inspiration.js.map +1 -1
  157. package/src/lib/plugin.js +4 -13
  158. package/src/lib/plugin.js.map +1 -1
  159. package/src/lib/providers/anthropic.js +9 -2
  160. package/src/lib/providers/anthropic.js.map +1 -1
  161. package/src/lib/providers/catalog.js +14 -5
  162. package/src/lib/providers/catalog.js.map +1 -1
  163. package/src/lib/providers/contract.d.ts +24 -1
  164. package/src/lib/providers/contract.js.map +1 -1
  165. package/src/lib/providers/routing.js +5 -2
  166. package/src/lib/providers/routing.js.map +1 -1
  167. package/src/lib/runtime/ai-dataset-generation.d.ts +120 -1
  168. package/src/lib/runtime/ai-dataset-generation.js +352 -3
  169. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  170. package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
  171. package/src/lib/runtime/ai-dev-replay.js +4 -2
  172. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  173. package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
  174. package/src/lib/runtime/ai-doctrine-validators.js +345 -28
  175. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  176. package/src/lib/runtime/ai-doctrine.d.ts +5 -4
  177. package/src/lib/runtime/ai-doctrine.js +10 -3
  178. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  179. package/src/lib/runtime/ai-icon-library.d.ts +27 -1
  180. package/src/lib/runtime/ai-icon-library.js +30 -3
  181. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  182. package/src/lib/runtime/ai-products-generation.d.ts +11 -1
  183. package/src/lib/runtime/ai-products-generation.js +51 -4
  184. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  185. package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
  186. package/src/lib/runtime/assist-answer-cache.js +5 -2
  187. package/src/lib/runtime/assist-answer-cache.js.map +1 -1
  188. package/src/lib/server/ai-allotments.js +7 -2
  189. package/src/lib/server/ai-allotments.js.map +1 -1
  190. package/src/lib/server/ai-experiment-versions.js +5 -5
  191. package/src/lib/server/ai-experiment-versions.js.map +1 -1
  192. package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
  193. package/src/lib/server/ai-host-permissions-roster.js +33 -0
  194. package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
  195. package/src/lib/server/ai-host-permissions.js +8 -6
  196. package/src/lib/server/ai-host-permissions.js.map +1 -1
  197. package/src/lib/server/ai-image-reads-route.d.ts +17 -0
  198. package/src/lib/server/ai-image-reads-route.js +106 -0
  199. package/src/lib/server/ai-image-reads-route.js.map +1 -0
  200. package/src/lib/server/ai-seo-apply.js +5 -5
  201. package/src/lib/server/ai-seo-apply.js.map +1 -1
  202. package/src/lib/server/ai-site-import-route.js +7 -4
  203. package/src/lib/server/ai-site-import-route.js.map +1 -1
  204. package/src/lib/server/assist-build.d.ts +15 -1
  205. package/src/lib/server/assist-build.js +30 -3
  206. package/src/lib/server/assist-build.js.map +1 -1
  207. package/src/lib/server/assist-chat.d.ts +11 -7
  208. package/src/lib/server/assist-chat.js +185 -48
  209. package/src/lib/server/assist-chat.js.map +1 -1
  210. package/src/lib/server/assist-docs-query.d.ts +51 -0
  211. package/src/lib/server/assist-docs-query.js +57 -0
  212. package/src/lib/server/assist-docs-query.js.map +1 -0
  213. package/src/lib/server/assist-jobs-context.d.ts +102 -0
  214. package/src/lib/server/assist-jobs-context.js +227 -0
  215. package/src/lib/server/assist-jobs-context.js.map +1 -0
  216. package/src/lib/server/assist-view-context.d.ts +8 -4
  217. package/src/lib/server/assist-view-context.js +12 -7
  218. package/src/lib/server/assist-view-context.js.map +1 -1
  219. package/src/lib/server.js +12 -0
  220. package/src/lib/server.js.map +1 -1
  221. package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
  222. package/src/lib/tools/ai-dataset-tool.js +24 -7
  223. package/src/lib/tools/ai-dataset-tool.js.map +1 -1
  224. package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
  225. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  226. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
  227. package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
  228. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-copy.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 aiDanglingWord,\n detectOffVoiceCopy,\n} from '../runtime/ai-doctrine-validators'\nimport { aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * The words of a layout language document, made to fit where they are shown\n * (AGL-3660). Every word is the model's; the compiler only cleans and fits\n * them: markup a person would see is taken out, a line too long for its place\n * ends at a clean boundary instead of mid-word, a line that would end on a\n * dangling word (\"…for you and\") loses it, and a phone number or an email\n * address the brief never gave is written as the bracketed gap the doctrine\n * asks for (rule 14) rather than shown as a fact.\n */\n\n/** Characters each place holds before its line is ended. */\nexport const AI_LAYOUT_TEXT_LIMITS = {\n /** A page's h1. */\n title: 70,\n /** A section's heading. */\n heading: 80,\n /** A card's, a step's or a sub-heading's title. */\n itemTitle: 60,\n eyebrow: 40,\n /** A button's label; the palette holds a label to 40. */\n label: 28,\n lede: 240,\n text: 420,\n itemText: 240,\n listItem: 140,\n stat: 14,\n statLabel: 60,\n question: 120,\n answer: 420,\n quote: 240,\n note: 200,\n /** An image's description, which is its alt text. */\n alt: 160,\n} as const\n\nexport type AiLayoutTextPlace = keyof typeof AI_LAYOUT_TEXT_LIMITS\n\n/** Inline markup a model writes out of habit, which a page would print literally. */\nconst MARKUP = /(\\*\\*|__|`|~~|^#{1,6}\\s+|^\\s*[-*•]\\s+)/gm\n\n/** A sentence's end inside a line. */\nconst SENTENCE_END = /[.!?…](?=[\"'”’)\\]]?\\s)/g\n\n/** A clause boundary inside a line: a comma, a semicolon, a colon or a dash, with what follows. */\nconst CLAUSE_END = /\\s*(?:[,;:]|\\s[—–-])\\s/g\n\n/** Punctuation that may not end a line it was cut back to. */\nconst TRAILING_JOINERS = /[\\s,;:—–-]+$/\n\n/** Collapses whitespace and takes out markup a page would print. */\nexport function aiLayoutCleanText(value: unknown): string {\n if (typeof value !== 'string') return ''\n return value\n .replace(MARKUP, '')\n // A JSON escape a model wrote out as text (\"\\\\u2014\") is the character it names.\n .replace(/\\\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * \"a\" and \"an\" made to agree with the word after them, in English copy\n * (AGL-3988): the beta.270 Sugar Hollow packages said \"A easygoing session\"\n * twice, in their records and on the home page's cards. Only where the\n * letters settle it: \"a\" before a word opening a, e, i or o becomes \"an\" —\n * never before \"one\" or \"once\", or \"eu\" (\"a European\"); \"an\" before a word\n * opening a consonant other than h becomes \"a\". A word opening u or h, or\n * one in capitals (\"an FAQ\"), is said as the model wrote it, and so is a\n * capital \"A\" inside a sentence, a letter rather than an article (\"Plan A\n * is\"). Any other language's text comes back as it is.\n */\nexport function aiLayoutArticles(text: string, language?: string | null): string {\n if (!text || aiLayoutSpeaksOtherThanEnglish(language)) return text\n return text.replace(\n /(?<![\\p{L}\\p{N}'’-])(a|an|A|An)(\\s+)([A-Za-z][\\p{L}\\p{N}'’-]*)/gu,\n (whole, article: string, space: string, word: string, at: number) => {\n // A capital opens a sentence; anywhere else \"A\" is a letter, never an article.\n if (/^A/.test(article) && !/(?:^|[.!?:][\"”’)]?\\s*)$/.test(text.slice(0, at))) return whole\n // An initialism is said by its letters, which the letters alone do not settle.\n if (/^[A-Z]{2,}/.test(word)) return whole\n const lower = word.toLowerCase()\n const vowel = /^[aeio]/.test(lower) && !/^(?:one(?![a-z])|once|eu|ewe)/.test(lower)\n const consonant = /^[bcdfgjklmnpqrstvwxyz]/.test(lower)\n const an = article.toLowerCase() === 'an'\n if (!an && vowel) return `${article === 'a' ? 'an' : 'An'}${space}${word}`\n if (an && consonant) return `${article === 'an' ? 'a' : 'A'}${space}${word}`\n return whole\n },\n )\n}\n\n/**\n * The line with every trailing dangling word taken off, as rule 14 reads one,\n * in the site's language (AGL-3828): a Spanish line cut to \"…para la\" loses\n * both words, as an English one cut to \"…for the\" does.\n */\nfunction withoutDanglingTail(line: string, language?: string): string {\n let text = line.replace(TRAILING_JOINERS, '').trim()\n for (let guard = 0; guard < 6; guard += 1) {\n const word = aiDanglingWord(text, language)\n if (!word) break\n text = text\n .slice(0, text.length - word.length)\n .replace(TRAILING_JOINERS, '')\n .trim()\n }\n return text\n}\n\n/**\n * A line held to the characters its place holds, ended where a reader expects\n * a line to end: at the last sentence's end that leaves at least half the\n * room, else the last clause boundary that does, else the last whole word. A\n * line that fits is returned clean, with any dangling tail taken off.\n */\nfunction fitText(value: unknown, place: AiLayoutTextPlace, language?: string): string {\n const text = aiLayoutArticles(aiLayoutCleanText(value), language)\n const limit = AI_LAYOUT_TEXT_LIMITS[place]\n if (text.length <= limit) return withoutDanglingTail(text, language)\n const room = text.slice(0, limit + 1)\n const half = Math.floor(limit * 0.4)\n const lastMatch = (pattern: RegExp): number => {\n let at = -1\n for (const match of room.matchAll(pattern)) {\n const end = (match.index ?? 0) + (pattern === SENTENCE_END ? 1 : 0)\n if (end <= limit && end >= half) at = end\n }\n return at\n }\n const sentence = lastMatch(SENTENCE_END)\n if (sentence > 0) return withoutDanglingTail(room.slice(0, sentence), language)\n const clause = lastMatch(CLAUSE_END)\n if (clause > 0) return withoutDanglingTail(room.slice(0, clause), language)\n const space = room.lastIndexOf(' ', limit)\n return withoutDanglingTail(\n space > half ? room.slice(0, space) : room.slice(0, limit),\n language,\n )\n}\n\n/** A phone number as people write one: seven or more digits with the usual separators. */\nconst PHONE = /\\(?\\+?\\d[\\d\\s().-]{6,}\\d/g\nconst EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g\n\n/** The digits of a phone number, which is what two spellings of one number share. */\nfunction digitsOf(value: string): string {\n return value.replace(/\\D/g, '')\n}\n\n/**\n * The line with every phone number and email address the brief did not give\n * written as the gap (rule 14): a site's contact details are facts, and a\n * model that writes one the brief lacks is inventing it. `facts` is every\n * word the job was given — the brief, the site's answers and its profile.\n */\nexport function aiLayoutGuardContacts(line: string, facts: string): string {\n const known = facts.toLowerCase()\n const knownDigits = new Set((facts.match(PHONE) ?? []).map(digitsOf))\n return line\n .replace(EMAIL, (email) =>\n known.includes(email.toLowerCase()) ? email : '[email address]',\n )\n .replace(PHONE, (phone) => {\n const digits = digitsOf(phone)\n // A year, a price or a count is not a phone number.\n if (digits.length < 7) return phone\n // One number with and without its country code is one number (AGL-3979):\n // \"+1 512 555 0142\" given admits \"(512) 555-0142\" written.\n const known = [...knownDigits].some(\n (given) => given === digits || (Math.min(given.length, digits.length) >= 7 && (given.endsWith(digits) || digits.endsWith(given)) && Math.abs(given.length - digits.length) <= 3),\n )\n return known ? phone : '[phone number]'\n })\n}\n\n/**\n * A line fitted to its place with invented contact details written as gaps;\n * `language` is the site's (see `aiLayoutFitText`).\n */\nexport function aiLayoutWords(\n value: unknown,\n place: AiLayoutTextPlace,\n facts: string,\n language?: string,\n): string {\n return aiLayoutFitText(\n aiLayoutGuardContacts(aiLayoutCleanText(value), facts),\n place,\n language,\n )\n}\n\n/**\n * A line held to the characters its place holds (see `fitText`), and none at\n * all where what is left is filler. `language` is the site's language code,\n * which says what a dangling word is (AGL-3828); absent, English.\n */\nexport function aiLayoutFitText(\n value: unknown,\n place: AiLayoutTextPlace,\n language?: string,\n): string {\n const text = fitText(value, place, language)\n return aiLayoutIsFiller(text) ? '' : text\n}\n\n/**\n * Whether a line is filler rule 14 refuses (\"lorem ipsum\", \"your text here\"):\n * such a line is no copy, and the compiler leaves it out rather than ship it.\n */\nexport function aiLayoutIsFiller(text: string): boolean {\n return detectOffVoiceCopy([{ at: 'line', text }]).length > 0\n}\n\n/** A quote that cannot be a real customer's, written as the gap the owner fills with one (rule 14). */\nexport function aiLayoutQuoteGap(value: unknown, language?: string): string {\n const text = aiLayoutFitText(value, 'quote', language)\n .replace(/^[\"“'‘]+|[\"”'’]+$/g, '')\n .trim()\n if (!text) return ''\n if (/^\\[.*\\]$/.test(text)) return text\n return `[${text.replace(/^\\[|\\]$/g, '')}]`\n}\n"],"names":["aiDanglingWord","detectOffVoiceCopy","aiLayoutSpeaksOtherThanEnglish","AI_LAYOUT_TEXT_LIMITS","title","heading","itemTitle","eyebrow","label","lede","text","itemText","listItem","stat","statLabel","question","answer","quote","note","alt","MARKUP","SENTENCE_END","CLAUSE_END","TRAILING_JOINERS","aiLayoutCleanText","value","replace","_","hex","String","fromCharCode","parseInt","trim","aiLayoutArticles","language","whole","article","space","word","at","test","slice","lower","toLowerCase","vowel","consonant","an","withoutDanglingTail","line","guard","length","fitText","place","limit","room","half","Math","floor","lastMatch","pattern","match","matchAll","end","index","sentence","clause","lastIndexOf","PHONE","EMAIL","digitsOf","aiLayoutGuardContacts","facts","known","knownDigits","Set","map","email","includes","phone","digits","some","given","min","endsWith","abs","aiLayoutWords","aiLayoutFitText","aiLayoutIsFiller","aiLayoutQuoteGap"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-copy.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 aiDanglingWord,\n detectOffVoiceCopy,\n} from '../runtime/ai-doctrine-validators'\nimport { aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * The words of a layout language document, made to fit where they are shown\n * (AGL-3660). Every word is the model's; the compiler only cleans and fits\n * them: markup a person would see is taken out, a line too long for its place\n * ends at a clean boundary instead of mid-word, a line that would end on a\n * dangling word (\"…for you and\") loses it, and a phone number or an email\n * address the brief never gave is written as the bracketed gap the doctrine\n * asks for (rule 14) rather than shown as a fact.\n */\n\n/** Characters each place holds before its line is ended. */\nexport const AI_LAYOUT_TEXT_LIMITS = {\n /** A page's h1. */\n title: 70,\n /** A section's heading. */\n heading: 80,\n /** A card's, a step's or a sub-heading's title. */\n itemTitle: 60,\n eyebrow: 40,\n /** A button's label; the palette holds a label to 40. */\n label: 28,\n lede: 240,\n text: 420,\n itemText: 240,\n listItem: 140,\n stat: 14,\n statLabel: 60,\n question: 120,\n answer: 420,\n quote: 240,\n note: 200,\n /** An image's description, which is its alt text. */\n alt: 160,\n} as const\n\nexport type AiLayoutTextPlace = keyof typeof AI_LAYOUT_TEXT_LIMITS\n\n/** Inline markup a model writes out of habit, which a page would print literally. */\nconst MARKUP = /(\\*\\*|__|`|~~|^#{1,6}\\s+|^\\s*[-*•]\\s+)/gm\n\n/** A sentence's end inside a line. */\nconst SENTENCE_END = /[.!?…](?=[\"'”’)\\]]?\\s)/g\n\n/** A clause boundary inside a line: a comma, a semicolon, a colon or a dash, with what follows. */\nconst CLAUSE_END = /\\s*(?:[,;:]|\\s[—–-])\\s/g\n\n/** Punctuation that may not end a line it was cut back to. */\nconst TRAILING_JOINERS = /[\\s,;:—–-]+$/\n\n/** Collapses whitespace and takes out markup a page would print. */\nexport function aiLayoutCleanText(value: unknown): string {\n if (typeof value !== 'string') return ''\n return value\n .replace(MARKUP, '')\n // A JSON escape a model wrote out as text (\"\\\\u2014\") is the character it names.\n .replace(/\\\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * \"a\" and \"an\" made to agree with the word after them, in English copy\n * (AGL-3988): the beta.270 Sugar Hollow packages said \"A easygoing session\"\n * twice, in their records and on the home page's cards. Only where the\n * letters settle it: \"a\" before a word opening a, e, i or o becomes \"an\" —\n * never before \"one\" or \"once\", or \"eu\" (\"a European\"); \"an\" before a word\n * opening a consonant other than h becomes \"a\". A word opening u or h, or\n * one in capitals (\"an FAQ\"), is said as the model wrote it, and so is a\n * capital \"A\" inside a sentence, a letter rather than an article (\"Plan A\n * is\"). Any other language's text comes back as it is.\n */\nexport function aiLayoutArticles(text: string, language?: string | null): string {\n if (!text || aiLayoutSpeaksOtherThanEnglish(language)) return text\n return text.replace(\n /(?<![\\p{L}\\p{N}'’-])(a|an|A|An)(\\s+)([A-Za-z][\\p{L}\\p{N}'’-]*)/gu,\n (whole, article: string, space: string, word: string, at: number) => {\n // A capital opens a sentence; anywhere else \"A\" is a letter, never an article.\n if (/^A/.test(article) && !/(?:^|[.!?:][\"”’)]?\\s*)$/.test(text.slice(0, at))) return whole\n // An initialism is said by its letters, which the letters alone do not settle.\n if (/^[A-Z]{2,}/.test(word)) return whole\n const lower = word.toLowerCase()\n const vowel = /^[aeio]/.test(lower) && !/^(?:one(?![a-z])|once|eu|ewe)/.test(lower)\n const consonant = /^[bcdfgjklmnpqrstvwxyz]/.test(lower)\n const an = article.toLowerCase() === 'an'\n if (!an && vowel) return `${article === 'a' ? 'an' : 'An'}${space}${word}`\n if (an && consonant) return `${article === 'an' ? 'a' : 'A'}${space}${word}`\n return whole\n },\n )\n}\n\n/**\n * The line with every trailing dangling word taken off, as rule 14 reads one,\n * in the site's language (AGL-3828): a Spanish line cut to \"…para la\" loses\n * both words, as an English one cut to \"…for the\" does.\n */\nfunction withoutDanglingTail(line: string, language?: string): string {\n let text = line.replace(TRAILING_JOINERS, '').trim()\n for (let guard = 0; guard < 6; guard += 1) {\n const word = aiDanglingWord(text, language)\n if (!word) break\n text = text\n .slice(0, text.length - word.length)\n .replace(TRAILING_JOINERS, '')\n .trim()\n }\n return text\n}\n\n/**\n * A line held to the characters its place holds, ended where a reader expects\n * a line to end: at the last sentence's end that leaves at least half the\n * room, else the last clause boundary that does, else the last whole word. A\n * line that fits is returned clean, with any dangling tail taken off.\n */\nfunction fitText(value: unknown, place: AiLayoutTextPlace, language?: string): string {\n if (place === 'eyebrow') return aiLayoutFitEyebrow(value, language)\n const text = aiLayoutArticles(aiLayoutCleanText(value), language)\n const limit = AI_LAYOUT_TEXT_LIMITS[place]\n if (text.length <= limit) return withoutDanglingTail(text, language)\n return boundaryCut(text, limit, language) ?? spaceCut(text, limit, language)\n}\n\n/** The least of a line's room a cut may leave: 40% of it. */\nconst leastOf = (limit: number) => Math.floor(limit * 0.4)\n\n/**\n * A line too long for its room ended at the last sentence's end that leaves\n * at least 40% of the room, else at the last clause boundary that does;\n * `null` where it has neither.\n */\nfunction boundaryCut(text: string, limit: number, language?: string): string | null {\n const room = text.slice(0, limit + 1)\n const half = leastOf(limit)\n const lastMatch = (pattern: RegExp): number => {\n let at = -1\n for (const match of room.matchAll(pattern)) {\n const end = (match.index ?? 0) + (pattern === SENTENCE_END ? 1 : 0)\n if (end <= limit && end >= half) at = end\n }\n return at\n }\n const sentence = lastMatch(SENTENCE_END)\n if (sentence > 0) return withoutDanglingTail(room.slice(0, sentence), language)\n const clause = lastMatch(CLAUSE_END)\n if (clause > 0) return withoutDanglingTail(room.slice(0, clause), language)\n return null\n}\n\n/** A line too long for its room ended at its last whole word, else at the room's edge. */\nfunction spaceCut(text: string, limit: number, language?: string): string {\n const room = text.slice(0, limit + 1)\n const space = room.lastIndexOf(' ', limit)\n return withoutDanglingTail(space > leastOf(limit) ? room.slice(0, space) : room.slice(0, limit), language)\n}\n\n// ── An eyebrow, fitted whole (AGL-4065) ────────────────────────────────────\n\n/**\n * What joins the parts of an eyebrow, in each language the compiler writes\n * (`ai-layout-labels.ts`): the conjunctions a coordinated part ends on, and\n * the prepositions a part opens after. A language with no row is cut, never\n * re-phrased.\n */\nconst EYEBROW_JOINS: Readonly<Record<string, Joins>> = {\n en: { and: ['and', '&'], at: ['in', 'for', 'of', 'at', 'to', 'with', 'from', 'by', 'on', 'near', 'around', 'across', 'serving'], place: ['in', 'for'] },\n es: { and: ['y', 'e', '&'], at: ['en', 'para', 'de', 'del', 'con', 'desde', 'cerca'], place: ['en', 'para'] },\n pt: { and: ['e', '&'], at: ['em', 'para', 'de', 'do', 'da', 'com', 'perto'], place: ['em', 'para'] },\n fr: { and: ['et', '&'], at: ['à', 'en', 'pour', 'de', 'du', 'avec', 'près'], place: ['à', 'en', 'pour'] },\n it: { and: ['e', 'ed', '&'], at: ['a', 'in', 'per', 'di', 'da', 'con', 'vicino'], place: ['a', 'in', 'per'] },\n de: { and: ['und', '&'], at: ['in', 'für', 'von', 'aus', 'mit', 'bei'], place: ['in', 'für'] },\n}\n\n/** A language's joining words: `and` ends a coordinated part, `at` opens one, `place` opens where a business is or whom it serves. */\ninterface Joins {\n and: readonly string[]\n at: readonly string[]\n place: readonly string[]\n}\n\n/** Words an English eyebrow cut back to a word may not end on, beside rule 14's dangling words. */\nconst EYEBROW_TAIL_EN: ReadonlySet<string> = new Set([\n 'in', 'of', 'for', 'and', 'or', 'nor', 'but', 'at', 'to', 'the', 'a', 'an', 'with', 'from', 'by', 'on', 'near', 'around', 'across',\n 'about', 'into', 'over', 'through', 'throughout', 'between', 'within', 'via', 'out', 'serving', 'your', 'our', 'their', 'my', '&',\n])\n\nfunction joinsOf(language?: string | null): Joins | null {\n if (!aiLayoutSpeaksOtherThanEnglish(language)) return EYEBROW_JOINS['en'] as Joins\n return EYEBROW_JOINS[String(language).toLowerCase().split(/[-_]/)[0] ?? ''] ?? null\n}\n\n/** A word's letters in any script: a capital opens a name. */\nconst NAME_WORD = /^\\p{Lu}[\\p{L}\\p{M}'’.-]*$/u\nconst escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/** Where each of `keep` stands in a line, whole words only, earliest first. */\nfunction keptSpans(text: string, keep: readonly string[]): Array<{ start: number; end: number }> {\n const spans: Array<{ start: number; end: number }> = []\n for (const word of keep) {\n const trimmed = word.trim()\n if (!trimmed) continue\n for (const match of text.matchAll(new RegExp(`(?<![\\\\p{L}\\\\p{N}])${escapeRegExp(trimmed)}(?![\\\\p{L}\\\\p{N}])`, 'giu'))) {\n spans.push({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length })\n }\n }\n return spans.sort((one, two) => one.start - two.start || two.end - one.end)\n}\n\n/** Whether a line still holds the words a span of the original named. */\nfunction stillHolds(line: string, words: string): boolean {\n return keptSpans(line, [words]).length > 0\n}\n\n/**\n * The lines an eyebrow becomes with ONE coordinated part taken out, each\n * still holding every word to keep: \"Lawn care and landscaping in New\n * Braunfels\" without \"Lawn care and\"; \"San Marcos motorcycle and dirt bike\n * repair\" without \"motorcycle and\"; a list without its first element. A part\n * is the one to three words between the last boundary before a conjunction\n * or a comma — the line's start, a word to keep, a preposition, a comma or\n * another conjunction — and it.\n */\nfunction withoutAPart(text: string, kept: ReadonlyArray<{ start: number; end: number }>, language?: string | null): string[] {\n const joins = joinsOf(language)\n if (!joins) return []\n const tokens = [...text.matchAll(/[^\\s]+/g)].map((match) => ({ word: match[0], start: match.index ?? 0, end: (match.index ?? 0) + match[0].length }))\n const bare = (word: string) => word.toLowerCase().replace(/[,;:]+$/, '')\n const insideKept = (start: number, end: number) => kept.some((span) => start < span.end && end > span.start)\n const out: string[] = []\n for (let at = 0; at < tokens.length; at += 1) {\n const token = tokens[at] as (typeof tokens)[number]\n const conjunction = joins.and.includes(bare(token.word))\n const comma = /,$/.test(token.word) && !insideKept(token.start, token.end - 1)\n if (!conjunction && !comma) continue\n // The part ends with the conjunction, or is the word the comma closes.\n const last = conjunction ? at - 1 : at\n if (last < 0) continue\n let first = last\n while (first > 0) {\n const before = tokens[first - 1] as (typeof tokens)[number]\n if (/[,;:]$/.test(before.word) || joins.and.includes(bare(before.word)) || joins.at.includes(bare(before.word)) || insideKept(before.start, before.end)) break\n first -= 1\n }\n if (last < first || last - first > 2) continue\n const start = (tokens[first] as (typeof tokens)[number]).start\n const end = token.end\n if (insideKept(start, end)) continue\n // Inside the line, a conjunction's part goes only where what follows it\n // shares a head with it (\"motorcycle and dirt bike repair\"): two words\n // or more to the next boundary, never \"big and small\".\n if (conjunction && start > 0) {\n let right = 0\n for (let next = at + 1; next < tokens.length; next += 1) {\n const word = tokens[next] as (typeof tokens)[number]\n if (joins.and.includes(bare(word.word)) || joins.at.includes(bare(word.word)) || insideKept(word.start, word.end)) break\n right += 1\n if (/[,;:]$/.test(word.word)) break\n }\n if (right < 2) continue\n }\n const line = `${text.slice(0, start)}${text.slice(end).replace(/^\\s+/, '')}`.replace(/\\s+/g, ' ').trim()\n if (!line || line === text) continue\n out.push(start === 0 ? line.charAt(0).toLocaleUpperCase() + line.slice(1) : line)\n }\n return out\n}\n\n/**\n * An eyebrow read from after a leading clause, up to \"in\", \"for\" or a comma:\n * \"Weekly mowing and seasonal cleanups for homeowners in New Braunfels\" read\n * from \"homeowners\". Every such start, longest first.\n */\nfunction fromAPreposition(text: string, language?: string | null): string[] {\n const joins = joinsOf(language)\n if (!joins) return []\n const words = text.split(' ')\n const out: string[] = []\n words.forEach((word, at) => {\n if (!joins.place.includes(word.toLowerCase()) && !/[,;:]$/.test(word)) return\n const rest = words.slice(at + 1).join(' ').trim()\n if (rest) out.push(rest.charAt(0).toLocaleUpperCase() + rest.slice(1))\n })\n return out\n}\n\n/**\n * A line cut back to a whole word that is not inside a name — a capital\n * before and after the cut — and, in English, not on a word that joins, a\n * word at a time, never under 40% of the room; else the line's plain cut.\n */\nfunction wordCut(text: string, limit: number, language?: string | null): string {\n const words = text.slice(0, limit + 1).split(' ')\n // The word the room cuts through is never kept.\n if (text.length > limit) words.pop()\n const next = text.split(' ')\n const english = !aiLayoutSpeaksOtherThanEnglish(language)\n for (let count = words.length; count > 0; count -= 1) {\n const line = next.slice(0, count).join(' ')\n if (line.length < leastOf(limit)) break\n const before = (next[count - 1] ?? '').replace(/[,;:]+$/, '')\n const after = (next[count] ?? '').replace(/[,;:.!?]+$/, '')\n if (NAME_WORD.test(before) && NAME_WORD.test(after) && !/[,;:]$/.test(next[count - 1] ?? '')) continue\n if (english && EYEBROW_TAIL_EN.has(before.toLowerCase())) continue\n const fitted = withoutDanglingTail(line, language ?? undefined)\n if (fitted && fitted.length <= limit) return fitted\n }\n return spaceCut(text, limit, language ?? undefined)\n}\n\n/**\n * An eyebrow fitted to its 40 characters whole (AGL-4065): a label over a\n * hero says what and where, and the 2026-10-25 booth-kit heroes printed\n * \"Lawn care and landscaping in New\" and \"San Marcos motorcycle and dirt\n * bike\" — the town cut in half, the head noun lost.\n *\n * An eyebrow that holds one of the site's own names (`keep`: its town, its\n * service area, its own name, `aiLayoutPlaceWords`) is, over its room, the\n * first of these that fits and still holds the first such name it held:\n *\n * 1. the line without one coordinated part — its first (\"Lawn care and\n * landscaping in New Braunfels\" → \"Landscaping in New Braunfels\"), or\n * one inside it that shares its head with the next (\"San Marcos\n * motorcycle and dirt bike repair\" → \"San Marcos dirt bike repair\") —\n * the longest that keeps every name;\n * 2. the line ended at its last sentence or clause boundary;\n * 3. the line cut back to a whole word (below);\n * 4. the line read from after a leading clause, up to \"in\", \"for\" or a\n * comma (\"Homeowners in New Braunfels\");\n * 5. else the cut of step 3, whatever it holds.\n *\n * One that holds none is ended at its last sentence or clause boundary, as\n * any line is, else cut back to a whole word that is neither inside a name\n * (a capital on both sides of the cut: \"New | Braunfels\") nor, in English,\n * a word that joins (\"…in\", \"…and\", \"…for\"). A line that fits is returned as\n * `aiLayoutFitText` returns one. The words are always the model's,\n * re-capitalized only after a cut at the front.\n */\nexport function aiLayoutFitEyebrow(value: unknown, language?: string | null, keep: readonly string[] = []): string {\n const text = aiLayoutArticles(aiLayoutCleanText(value), language ?? undefined)\n const limit = AI_LAYOUT_TEXT_LIMITS.eyebrow\n if (text.length <= limit) return withoutDanglingTail(text, language ?? undefined)\n const kept = keptSpans(text, keep)\n const boundary = boundaryCut(text, limit, language ?? undefined)\n const cut = () => wordCut(text, limit, language)\n if (!kept.length) return boundary ?? cut()\n const firstKept = text.slice((kept[0] as { start: number }).start, (kept[0] as { end: number }).end)\n const holds = (line: string | null): line is string => !!line && line.length <= limit && !!aiLayoutCleanText(line) && stillHolds(line, firstKept)\n const keepsAll = (line: string) => kept.every((span) => stillHolds(line, text.slice(span.start, span.end)))\n const parts = withoutAPart(text, kept, language)\n .map((line) => withoutDanglingTail(line, language ?? undefined))\n .filter((line) => holds(line) && keepsAll(line))\n const longest = parts.reduce<string | null>((best, line) => (best === null || line.length > best.length ? line : best), null)\n if (longest) return longest\n if (holds(boundary)) return boundary\n const words = cut()\n if (holds(words)) return words\n const after = fromAPreposition(text, language)\n .map((line) => withoutDanglingTail(line, language ?? undefined))\n .find((line) => holds(line))\n return after ?? words\n}\n\n/** A line of the job's words that is a name: one to four words, each opening on a capital, no digits. */\nconst NAME_LINE = /^(?:\\p{Lu}[\\p{L}\\p{M}'’.-]*)(?:\\s+\\p{Lu}[\\p{L}\\p{M}'’.-]*){0,3}$/u\n\nlet placeWordsCache: { facts: string; words: string[] } | null = null\n\n/**\n * The names among the words a job was given (AGL-4065): each line, or each\n * part of a line between commas, that is a name of one to four capitalized\n * words — the city (\"New Braunfels\"), each town of its service area, its\n * region (\"TX\"), its own name — which an eyebrow is fitted around, never\n * through.\n */\nexport function aiLayoutPlaceWords(facts: string): string[] {\n if (placeWordsCache?.facts === facts) return placeWordsCache.words\n const words = [\n ...new Set(\n facts\n .split(/\\n|[,;·|]/)\n .map((part) => part.trim().replace(/[.:]+$/, ''))\n .filter((part) => part.length >= 2 && !/\\d/.test(part) && NAME_LINE.test(part)),\n ),\n ]\n placeWordsCache = { facts, words }\n return words\n}\n\n/** A phone number as people write one: seven or more digits with the usual separators. */\nconst PHONE = /\\(?\\+?\\d[\\d\\s().-]{6,}\\d/g\nconst EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g\n\n/** The digits of a phone number, which is what two spellings of one number share. */\nfunction digitsOf(value: string): string {\n return value.replace(/\\D/g, '')\n}\n\n/**\n * The line with every phone number and email address the brief did not give\n * written as the gap (rule 14): a site's contact details are facts, and a\n * model that writes one the brief lacks is inventing it. `facts` is every\n * word the job was given — the brief, the site's answers and its profile.\n */\nexport function aiLayoutGuardContacts(line: string, facts: string): string {\n const known = facts.toLowerCase()\n const knownDigits = new Set((facts.match(PHONE) ?? []).map(digitsOf))\n return line\n .replace(EMAIL, (email) =>\n known.includes(email.toLowerCase()) ? email : '[email address]',\n )\n .replace(PHONE, (phone) => {\n const digits = digitsOf(phone)\n // A year, a price or a count is not a phone number.\n if (digits.length < 7) return phone\n // One number with and without its country code is one number (AGL-3979):\n // \"+1 512 555 0142\" given admits \"(512) 555-0142\" written.\n const known = [...knownDigits].some(\n (given) => given === digits || (Math.min(given.length, digits.length) >= 7 && (given.endsWith(digits) || digits.endsWith(given)) && Math.abs(given.length - digits.length) <= 3),\n )\n return known ? phone : '[phone number]'\n })\n}\n\n/**\n * A line fitted to its place with invented contact details written as gaps;\n * `language` is the site's (see `aiLayoutFitText`).\n */\nexport function aiLayoutWords(\n value: unknown,\n place: AiLayoutTextPlace,\n facts: string,\n language?: string,\n): string {\n const guarded = aiLayoutGuardContacts(aiLayoutCleanText(value), facts)\n // An eyebrow is fitted around the site's own names (AGL-4065).\n if (place === 'eyebrow') {\n const text = aiLayoutFitEyebrow(guarded, language, aiLayoutPlaceWords(facts))\n return aiLayoutIsFiller(text) ? '' : text\n }\n return aiLayoutFitText(guarded, place, language)\n}\n\n/**\n * A line held to the characters its place holds (see `fitText`), and none at\n * all where what is left is filler. `language` is the site's language code,\n * which says what a dangling word is (AGL-3828); absent, English.\n */\nexport function aiLayoutFitText(\n value: unknown,\n place: AiLayoutTextPlace,\n language?: string,\n): string {\n const text = fitText(value, place, language)\n return aiLayoutIsFiller(text) ? '' : text\n}\n\n/**\n * Whether a line is filler rule 14 refuses (\"lorem ipsum\", \"your text here\"):\n * such a line is no copy, and the compiler leaves it out rather than ship it.\n */\nexport function aiLayoutIsFiller(text: string): boolean {\n return detectOffVoiceCopy([{ at: 'line', text }]).length > 0\n}\n\n/** A quote that cannot be a real customer's, written as the gap the owner fills with one (rule 14). */\nexport function aiLayoutQuoteGap(value: unknown, language?: string): string {\n const text = aiLayoutFitText(value, 'quote', language)\n .replace(/^[\"“'‘]+|[\"”'’]+$/g, '')\n .trim()\n if (!text) return ''\n if (/^\\[.*\\]$/.test(text)) return text\n return `[${text.replace(/^\\[|\\]$/g, '')}]`\n}\n"],"names":["aiDanglingWord","detectOffVoiceCopy","aiLayoutSpeaksOtherThanEnglish","AI_LAYOUT_TEXT_LIMITS","title","heading","itemTitle","eyebrow","label","lede","text","itemText","listItem","stat","statLabel","question","answer","quote","note","alt","MARKUP","SENTENCE_END","CLAUSE_END","TRAILING_JOINERS","aiLayoutCleanText","value","replace","_","hex","String","fromCharCode","parseInt","trim","aiLayoutArticles","language","whole","article","space","word","at","test","slice","lower","toLowerCase","vowel","consonant","an","withoutDanglingTail","line","guard","length","fitText","place","boundaryCut","aiLayoutFitEyebrow","limit","spaceCut","leastOf","Math","floor","room","half","lastMatch","pattern","match","matchAll","end","index","sentence","clause","lastIndexOf","EYEBROW_JOINS","en","and","es","pt","fr","it","de","EYEBROW_TAIL_EN","Set","joinsOf","split","NAME_WORD","escapeRegExp","keptSpans","keep","spans","trimmed","RegExp","push","start","sort","one","two","stillHolds","words","withoutAPart","kept","joins","tokens","map","bare","insideKept","some","span","out","token","conjunction","includes","comma","last","first","before","right","next","charAt","toLocaleUpperCase","fromAPreposition","forEach","rest","join","wordCut","pop","english","count","after","has","fitted","undefined","boundary","cut","firstKept","holds","keepsAll","every","parts","filter","longest","reduce","best","find","NAME_LINE","placeWordsCache","aiLayoutPlaceWords","facts","part","PHONE","EMAIL","digitsOf","aiLayoutGuardContacts","known","knownDigits","email","phone","digits","given","min","endsWith","abs","aiLayoutWords","guarded","aiLayoutIsFiller","aiLayoutFitText","aiLayoutQuoteGap"],"mappings":"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"}
@@ -116,6 +116,25 @@ export interface AiLayoutDesignChoices {
116
116
  * cover beside each title — the two ways an editorial site lists writing.
117
117
  */
118
118
  writing: 'grid' | 'ruled';
119
+ /**
120
+ * How the titles of a designed group read (AGL-4065): `sentence`, each set
121
+ * in the theme's heading style for an item; or `caps`, each set as the
122
+ * theme's label (its overline: spaced capitals where the site's eyebrows
123
+ * are capitals) over its words, as a designer's ruled columns and icon
124
+ * rows title their items. Only a variant the theme defines is chosen: the
125
+ * case of a section's own heading is the look's font pairing, never the
126
+ * compiler's.
127
+ */
128
+ headingCase: 'sentence' | 'caps';
129
+ /**
130
+ * How the plain and soft bands run down a page (AGL-4065): `alternate`, the
131
+ * bands as the model wrote them except that two neighbors of one never
132
+ * meet — the second takes the other — so sections read apart; or `plain`,
133
+ * one ground for the whole page, every soft band drawn plain and the
134
+ * sections set apart by their spacing. Brand and dark bands are the
135
+ * model's either way.
136
+ */
137
+ bandRhythm: 'alternate' | 'plain';
119
138
  }
120
139
  type Family = 'gallery' | 'editorial' | 'hospitality' | 'calm' | 'retail' | 'standard';
121
140
  type GroupStyle = AiLayoutDesignChoices['group'];
@@ -99,18 +99,20 @@ const MOSTLY_MOSAIC = [
99
99
  splitHeads: 0.5,
100
100
  pictures: MOSTLY_MOSAIC
101
101
  },
102
- // Direct: a full-bleed job photo with the promise at the start, services as tight cards.
102
+ // Direct: a full-bleed job photo with the promise at the start, services as
103
+ // tight cards, ruled columns or icon rows, and a job's steps numbered or
104
+ // ruled (AGL-4065: two trades sites of 2026-10-25 shared one layout).
103
105
  trades: {
104
106
  heroes: [
105
107
  'cover',
106
108
  'split'
107
109
  ],
108
110
  groups: [
109
- 'cards'
110
- ],
111
- steps: [
112
- 'numbers'
111
+ 'cards',
112
+ 'ruled',
113
+ 'icons'
113
114
  ],
115
+ steps: BOTH_STEPS,
114
116
  features: 0.5,
115
117
  coverClose: 0.7,
116
118
  splitHeads: 0.2,
@@ -276,17 +278,17 @@ const MOSTLY_MOSAIC = [
276
278
  pictures: MOSTLY_MOSAIC
277
279
  },
278
280
  // High energy: half the band a photo, half a loud headline; or full-bleed.
281
+ // Its groups as cards or ruled columns, its steps numbered or ruled (AGL-4065).
279
282
  fitness: {
280
283
  heroes: [
281
284
  'bleed',
282
285
  'cover'
283
286
  ],
284
287
  groups: [
285
- 'cards'
286
- ],
287
- steps: [
288
- 'numbers'
288
+ 'cards',
289
+ 'ruled'
289
290
  ],
291
+ steps: BOTH_STEPS,
290
292
  features: 0.5,
291
293
  coverClose: 0.7,
292
294
  splitHeads: 0.2,
@@ -464,6 +466,14 @@ const pick = (next, list)=>list[Math.floor(next() * list.length) % list.length];
464
466
  var _FAMILIES_kind;
465
467
  return (_FAMILIES_kind = FAMILIES[kind]) != null ? _FAMILIES_kind : 'standard';
466
468
  }
469
+ /** The kinds whose item titles are set as labels three sites in ten (AGL-4065); every other kind's one in ten. */ const LABEL_TITLE_KINDS = new Set([
470
+ 'trades',
471
+ 'fitness'
472
+ ]);
473
+ /** The families whose pages keep one ground as often as they alternate: a gallery's, a publication's (AGL-4065). */ const ONE_GROUND_FAMILIES = new Set([
474
+ 'gallery',
475
+ 'editorial'
476
+ ]);
467
477
  /**
468
478
  * A site's choices, the same for every page of it, each drawn at even odds
469
479
  * among the options its kind's signature lists (AGL-3899, AGL-3985), so two
@@ -499,7 +509,10 @@ const pick = (next, list)=>list[Math.floor(next() * list.length) % list.length];
499
509
  writing: pick(next, [
500
510
  'grid',
501
511
  'ruled'
502
- ])
512
+ ]),
513
+ // Drawn after it (AGL-4065), so a seed's earlier choices stay too.
514
+ headingCase: next() < (LABEL_TITLE_KINDS.has(design.kind) ? 0.3 : 0.1) ? 'caps' : 'sentence',
515
+ bandRhythm: next() < (ONE_GROUND_FAMILIES.has(family) ? 0.5 : 0.35) ? 'plain' : 'alternate'
503
516
  };
504
517
  }
505
518
  const WORK = /\b(work|works|project|projects|portfolio|gallery|galleries|case|cases|studies|selected|commission|commissions|illustration|illustrations|photo|photos|series|book|books|editorial|brand|collection|collections|wedding|weddings|portrait|portraits|session|sessions|shoot|shoots)\b/i;
@@ -508,7 +521,14 @@ const DISHES = /\b(menu|menus|dish|dishes|breakfast|brunch|lunch|dinner|pastry|p
508
521
  // A store's own products are listed by its Product grid (AGL-3676), never drawn
509
522
  // from words: what a store shows as pictures is how its range is grouped — its
510
523
  // collections, gift sets, occasions and scents. "Candle care and burn notes" is no band of pictures.
511
- /** Words a section uses for itself when it is a showing of pictures, on any kind of site (AGL-3923). */ const GALLERY = /\b(gallery|galleries|portfolio|portfolios|lookbook|lookbooks)\b/i;
524
+ /**
525
+ * Words a section uses for itself when it is a showing of pictures, on any
526
+ * kind of site (AGL-3923) — "in pictures" and "in photos" among them
527
+ * (AGL-4065): the 2026-10-25 Rut Runners Racing and Pit Row Tacos homes drew
528
+ * "The team at the track in pictures" and "The trailer and the food in
529
+ * pictures" as six captions each, with no picture slot for a photo to fill.
530
+ * Whole words only: "Picture-perfect" and "Photoshoot" show no pictures.
531
+ */ const GALLERY = /\b(gallery|galleries|portfolio|portfolios|lookbook|lookbooks|pictures|photos)\b/i;
512
532
  const CATEGORIES = /\b(collection|collections|range|ranges|categor(?:y|ies)|gift|gifts|gift sets?|sets|occasion|occasions|scent|scents|edit|edits)\b/i;
513
533
  /**
514
534
  * How a section's group of cards is drawn, from what the section is about:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-design.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\n/**\n * THE DESIGNER LAYER of the layout compiler (AGL-3660): how a page the model\n * described in the layout language is ARRANGED, decided by code from the\n * site's kind and its job's seed — never asked of the model, so a recorded\n * answer compiles into the new design with no new call.\n *\n * Why. A page compiled one way for every site read like a SaaS landing page:\n * a centered text hero, three outlined cards, a numbered row, a solid form\n * band. The designer templates people compare a site to (Squarespace's Loam,\n * Clove, Auburn, Rivoli, Matsuya; Wix's portfolio and restaurant sets) are\n * built from a handful of moves instead — a full-bleed photo hero with the\n * type over it, image and text side by side, work shown as pictures in an\n * uneven grid, writing as a list under thin rules, a closing band over a\n * photo — and they pick those moves for the KIND of site.\n *\n * So a site's design is a set of VARIANTS — the hero's arrangement, how its\n * groups are drawn, whether its text sections take a picture beside them,\n * how its closing section reads — each chosen among the ones that suit the\n * kind, by the seed. The choices are the site's: every page of one site is\n * drawn with the same hero and the same group style, so the site reads as\n * one design, and two sites of one kind come out differently.\n *\n * This is NOT a template library. A variant is a way of drawing whatever the\n * model wrote; the sections, their order, their bands, their words and how\n * many items each shows are still the model's, page by page.\n *\n * Pure: no tree, no theme, no network.\n */\n\n/** What the compiler is told about the site a page belongs to. */\nexport interface AiLayoutDesign {\n /** The site's kind (`AI_SITE_KINDS`); an unknown one draws as a business site. */\n kind: string\n /** The job's design seed: the same for every page of one site. */\n seed: number\n /** Whether this page is the site's home, which opens tallest. */\n home: boolean\n}\n\n/**\n * How the first section of a page is drawn:\n * - `cover`: a full-bleed photo with the words over it on a scrim, set at\n * the start of the page;\n * - `poster`: a full-bleed photo, taller, the words centered over it in\n * display type (AGL-3985);\n * - `split`: the words beside a tall photo, inside the page's measure;\n * - `bleed`: the band in two halves, a photo filling one edge to edge and\n * the words in the other (AGL-3985);\n * - `editorial`: display type across the page, the lede and the buttons in\n * a row under it, and a wide photo below;\n * - `column`: a masthead — the words centered in a narrow column, a wide\n * photo under them (AGL-3985);\n * - `collage`: the words beside two photos, a tall one and a smaller one\n * laid over its corner (AGL-3985);\n * - `panel`: a full-bleed photo, undimmed, with the words on a card of the\n * page's surface over its lower corner (AGL-3985);\n * - `portrait`: a round photo over the words, all centered: a person's own\n * page (AGL-3985).\n */\nexport type AiLayoutHeroVariant = 'cover' | 'poster' | 'split' | 'bleed' | 'editorial' | 'column' | 'collage' | 'panel' | 'portrait'\n\n/** Every hero arrangement, as {@link AiLayoutHeroVariant} lists them. */\nexport const AI_LAYOUT_HERO_VARIANTS: readonly AiLayoutHeroVariant[] = ['cover', 'poster', 'split', 'bleed', 'editorial', 'column', 'collage', 'panel', 'portrait']\n\n/** The heroes whose words take half the page or less, so their title is the page's title size, never display. */\nexport const AI_LAYOUT_HALF_HEROES: ReadonlySet<AiLayoutHeroVariant> = new Set(['split', 'bleed', 'collage', 'panel'])\n\n/** The heroes that lay their words over a photo, which pins the section to the site's dark scheme. */\nexport const AI_LAYOUT_OVER_PHOTO_HEROES: ReadonlySet<AiLayoutHeroVariant> = new Set(['cover', 'poster'])\n\n/**\n * How a group of cards is drawn where no component draws it:\n * - `cards`: the theme's cards (the compiler's first design);\n * - `ruled`: open columns, each under a thin rule, no card around it;\n * - `pictures`: a picture over each item's title and caption, in an uneven\n * grid (work, products);\n * - `articles`: a picture over each title and summary, in even columns (writing);\n * - `menu`: two columns of lines under rules (dishes, prices left out);\n * - `icons`: open columns, each led by an icon in a soft disc over its title\n * and words — a storefront's reasons to buy (shipping, small batch,\n * returns), never a block of text (AGL-3676).\n */\nexport type AiLayoutGroupVariant = 'cards' | 'ruled' | 'pictures' | 'articles' | 'menu' | 'icons'\n\n/** How a numbered group is drawn: a row of numbers, or a ruled list beside its heading. */\nexport type AiLayoutStepsVariant = 'numbers' | 'timeline'\n\n/** The site-level choices a page is drawn with. */\nexport interface AiLayoutDesignChoices {\n hero: AiLayoutHeroVariant\n /** The group style of a section that names no subject the kind draws its own way. */\n group: 'cards' | 'ruled' | 'icons'\n steps: AiLayoutStepsVariant\n /** Whether a section of words takes a photo beside it. */\n features: boolean\n /** Whether the photo of the first such section sits on the left. */\n featureLeft: boolean\n /** Whether a closing section of words is drawn over a photo. */\n coverClose: boolean\n /** Whether a section's heading sits beside its items rather than over them. */\n splitHeads: boolean\n /** How work is laid out where the kind shows it as pictures. */\n pictures: 'mosaic' | 'even'\n /**\n * How a blog's own posts are listed where a page shows them (AGL-3676):\n * `grid`, a cover over each post's date, title and excerpt in columns; or\n * `ruled`, a list under thin rules, the date in its own column and a small\n * cover beside each title — the two ways an editorial site lists writing.\n */\n writing: 'grid' | 'ruled'\n}\n\ntype Family = 'gallery' | 'editorial' | 'hospitality' | 'calm' | 'retail' | 'standard'\ntype GroupStyle = AiLayoutDesignChoices['group']\n\nconst FAMILIES: Readonly<Record<string, Family>> = {\n portfolio: 'gallery',\n photography: 'gallery',\n studio: 'gallery',\n blog: 'editorial',\n restaurant: 'hospitality',\n events: 'hospitality',\n yoga: 'calm',\n wellness: 'calm',\n beauty: 'calm',\n store: 'retail',\n}\n\n/**\n * A kind's LAYOUT SIGNATURE (AGL-3985): the arrangements its pages are drawn\n * in, so a café, a law firm and a band never read as one page in three\n * colors. Before it, six kinds shared one list of heroes and every kind drew\n * its groups, its photo-led sections and its headings beside their items at\n * the same odds; the guided start's 21 style previews, which the real\n * compiler draws, read as one page — a header bar, a split or banded hero,\n * one section.\n *\n * - `heroes`: the two first sections the kind opens with, each drawn at even\n * odds (AGL-3899). The first is the one its style card shows. No two kinds\n * share the same two.\n * - `groups`: the group styles a section of items takes where its subject\n * picks none.\n * - `steps`: how its numbered groups read.\n * - `features`: the chance a section of words takes a photo beside it — a\n * café's or a salon's pages are photo-led, a law firm's or a writer's let\n * the type carry them.\n * - `coverClose`: the chance a closing call to action is drawn over a photo.\n * - `splitHeads`: the chance a heading sits beside its items: an\n * editorial, considered page.\n * - `pictures`: how its work is laid out where it shows it as pictures.\n */\nexport interface AiLayoutKindSignature {\n heroes: readonly [AiLayoutHeroVariant, AiLayoutHeroVariant]\n groups: readonly GroupStyle[]\n steps: readonly AiLayoutStepsVariant[]\n features: number\n coverClose: number\n splitHeads: number\n pictures: readonly AiLayoutDesignChoices['pictures'][]\n}\n\nconst BOTH_STEPS: readonly AiLayoutStepsVariant[] = ['timeline', 'numbers']\nconst MOSTLY_MOSAIC: readonly AiLayoutDesignChoices['pictures'][] = ['mosaic', 'mosaic', 'even']\n\n/** Each kind's signature, by its `AI_SITE_KINDS` id. */\nexport const AI_LAYOUT_KIND_SIGNATURES: Readonly<Record<string, AiLayoutKindSignature>> = {\n // Clear and confident: a contained split, or the words on a card over the place.\n business: { heroes: ['split', 'panel'], groups: ['cards', 'icons'], steps: BOTH_STEPS, features: 0.6, coverClose: 0.3, splitHeads: 0.5, pictures: MOSTLY_MOSAIC },\n // Direct: a full-bleed job photo with the promise at the start, services as tight cards.\n trades: { heroes: ['cover', 'split'], groups: ['cards'], steps: ['numbers'], features: 0.5, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Measured: the words on a card over the office, practice areas ruled, headings beside them.\n professional: { heroes: ['panel', 'editorial'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.2, splitHeads: 0.8, pictures: MOSTLY_MOSAIC },\n // Reassuring: half the band a calm room, or a quiet centered masthead.\n wellness: { heroes: ['bleed', 'column'], groups: ['icons', 'cards'], steps: BOTH_STEPS, features: 0.7, coverClose: 0.2, splitHeads: 0.3, pictures: MOSTLY_MOSAIC },\n // Photo-led: the room full-bleed with the name centered over it, the menu as lines.\n restaurant: { heroes: ['poster', 'bleed'], groups: ['ruled', 'cards'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // A storefront: the products themselves, two of them, beside the words.\n store: { heroes: ['collage', 'split'], groups: ['icons'], steps: BOTH_STEPS, features: 1, coverClose: 0.2, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // The work first: display type then a wide picture, or the words beside two pieces.\n portfolio: { heroes: ['editorial', 'collage'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.2, splitHeads: 0.5, pictures: ['mosaic'] },\n // Oversized statements over a wide picture, or full-bleed and dark.\n studio: { heroes: ['editorial', 'cover'], groups: ['ruled', 'cards'], steps: ['timeline'], features: 0.4, coverClose: 0.5, splitHeads: 0.6, pictures: ['mosaic', 'even'] },\n // Pictures first, few words.\n photography: { heroes: ['editorial', 'poster'], groups: ['ruled'], steps: ['timeline'], features: 0.6, coverClose: 0.6, splitHeads: 0.35, pictures: ['even', 'mosaic'] },\n // Reading-first: a centered masthead over the lead picture.\n blog: { heroes: ['column', 'editorial'], groups: ['ruled'], steps: ['timeline'], features: 0.4, coverClose: 0.1, splitHeads: 0.7, pictures: MOSTLY_MOSAIC },\n // Celebratory: two pictures of the day beside its name, or the name over one.\n events: { heroes: ['collage', 'poster'], groups: ['cards', 'icons'], steps: ['timeline'], features: 0.7, coverClose: 0.6, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // High energy: half the band a photo, half a loud headline; or full-bleed.\n fitness: { heroes: ['bleed', 'cover'], groups: ['cards'], steps: ['numbers'], features: 0.5, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Calm and spacious: a centered masthead, or a soft split.\n yoga: { heroes: ['column', 'split'], groups: ['icons', 'cards'], steps: BOTH_STEPS, features: 0.6, coverClose: 0.2, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Image-led: two pictures of the work and the place, or half the band one.\n beauty: { heroes: ['collage', 'bleed'], groups: ['ruled', 'cards'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.3, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Polished: the home full-bleed with the words on a card, or under a scrim.\n realestate: { heroes: ['panel', 'cover'], groups: ['cards', 'ruled'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.5, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Encouraging: a contained split, or the teacher's portrait over the words.\n education: { heroes: ['split', 'portrait'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.6, coverClose: 0.2, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Mission-led: the people full-bleed with the cause over them, or two pictures beside it.\n nonprofit: { heroes: ['cover', 'collage'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.6, coverClose: 0.7, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Loud: the name huge over a full-bleed photo, at the start or centered.\n music: { heroes: ['cover', 'poster'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.8, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // A person: their portrait over who they are, or a quiet masthead.\n personal: { heroes: ['portrait', 'column'], groups: ['ruled'], steps: ['timeline'], features: 0.4, coverClose: 0.1, splitHeads: 0.6, pictures: MOSTLY_MOSAIC },\n // One action: the promise centered over the picture of the thing, or over a photo.\n landing: { heroes: ['column', 'poster'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.5, coverClose: 0.3, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Short and striking: one big line over a photo, or on a card over it.\n 'coming-soon': { heroes: ['poster', 'panel'], groups: ['icons', 'ruled'], steps: BOTH_STEPS, features: 0.3, coverClose: 0.2, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n}\n\n/** The signature of a kind; an unknown kind draws as a business site. */\nexport function aiLayoutKindSignature(kind: string): AiLayoutKindSignature {\n return AI_LAYOUT_KIND_SIGNATURES[kind] ?? (AI_LAYOUT_KIND_SIGNATURES['business'] as AiLayoutKindSignature)\n}\n\n/** A small deterministic generator over a seed (mulberry32). */\nfunction random(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\nconst pick = <T>(next: () => number, list: readonly T[]): T => list[Math.floor(next() * list.length) % list.length] as T\n\n/** The family a kind's pages are drawn in. */\nexport function aiLayoutFamily(kind: string): Family {\n return FAMILIES[kind] ?? 'standard'\n}\n\n/**\n * A site's choices, the same for every page of it, each drawn at even odds\n * among the options its kind's signature lists (AGL-3899, AGL-3985), so two\n * sites of one kind are drawn apart and two kinds never read as one page.\n * Favoring each list's first option made the newest production sites one\n * design: the kind's first hero won 55% before the pick, a business site\n * opened split, groups were ruled two times in three and every text section\n * took a photo.\n */\nexport function aiLayoutDesignChoices(design: AiLayoutDesign): AiLayoutDesignChoices {\n const next = random(design.seed ^ 0x2f6b9d1)\n const family = aiLayoutFamily(design.kind)\n const signature = aiLayoutKindSignature(design.kind)\n const hero = pick(next, signature.heroes)\n const group = pick(next, signature.groups)\n return {\n hero,\n // A store's reasons to buy — the making, the shipping, the care — read\n // as open columns each led by an icon, the way a storefront theme draws\n // them: never a SaaS page's feature cards, and never the plain columns\n // of a title and a sentence the beta.237 Willow Wick home showed four\n // bands of (AGL-3676).\n group: family === 'retail' ? 'icons' : group,\n steps: pick(next, signature.steps),\n // Whether a section of words takes a photo beside it: some sites are\n // photo-led, some let their type carry the page. A storefront never\n // shows a band of text alone (AGL-3676), so its story always takes one.\n features: next() < signature.features || family === 'retail',\n featureLeft: next() < 0.5,\n coverClose: next() < signature.coverClose,\n splitHeads: next() < signature.splitHeads,\n pictures: pick(next, signature.pictures),\n // Drawn last, so every choice above stays what it was for a seed.\n writing: pick(next, ['grid', 'ruled'] as const),\n }\n}\n\nconst WORK = /\\b(work|works|project|projects|portfolio|gallery|galleries|case|cases|studies|selected|commission|commissions|illustration|illustrations|photo|photos|series|book|books|editorial|brand|collection|collections|wedding|weddings|portrait|portraits|session|sessions|shoot|shoots)\\b/i\nconst WRITING = /\\b(article|articles|post|posts|writing|writings|latest|featured|journey|journeys|recipe|recipes|story|stories|essay|essays|guide|guides|issue|issues|episode|episodes)\\b/i\nconst DISHES = /\\b(menu|menus|dish|dishes|breakfast|brunch|lunch|dinner|pastry|pastries|coffee|drink|drinks|cocktail|cocktails|wine|wines|highlight|highlights|favorite|favorites|favourites|special|specials|plate|plates|bake|bakes|bread|cake|cakes)\\b/i\n// A store's own products are listed by its Product grid (AGL-3676), never drawn\n// from words: what a store shows as pictures is how its range is grouped — its\n// collections, gift sets, occasions and scents. \"Candle care and burn notes\" is no band of pictures.\n/** Words a section uses for itself when it is a showing of pictures, on any kind of site (AGL-3923). */\nconst GALLERY = /\\b(gallery|galleries|portfolio|portfolios|lookbook|lookbooks)\\b/i\nconst CATEGORIES = /\\b(collection|collections|range|ranges|categor(?:y|ies)|gift|gifts|gift sets?|sets|occasion|occasions|scent|scents|edit|edits)\\b/i\n\n/**\n * How a section's group of cards is drawn, from what the section is about:\n * work in a gallery kind and products in a store are pictures, writing in a\n * blog is articles, dishes in a restaurant a menu; anything else takes the\n * site's own group style.\n */\nexport function aiLayoutGroupVariant(\n design: AiLayoutDesign,\n choices: AiLayoutDesignChoices,\n words: string,\n): AiLayoutGroupVariant {\n const family = aiLayoutFamily(design.kind)\n if (family === 'gallery' && (WORK.test(words) || design.kind === 'photography')) return 'pictures'\n if (family === 'editorial' && WRITING.test(words)) return 'articles'\n if (family === 'hospitality' && DISHES.test(words)) return 'menu'\n if (family === 'retail' && CATEGORIES.test(words)) return 'pictures'\n // A section that calls itself a gallery or a portfolio shows pictures on any\n // kind of site (AGL-3923): the beta.265 Kasvora Marketing Solutions home drew\n // its \"Selected work gallery\" as four cards of icons.\n if (GALLERY.test(words)) return 'pictures'\n return choices.group\n}\n"],"names":["AI_LAYOUT_HERO_VARIANTS","AI_LAYOUT_HALF_HEROES","Set","AI_LAYOUT_OVER_PHOTO_HEROES","FAMILIES","portfolio","photography","studio","blog","restaurant","events","yoga","wellness","beauty","store","BOTH_STEPS","MOSTLY_MOSAIC","AI_LAYOUT_KIND_SIGNATURES","business","heroes","groups","steps","features","coverClose","splitHeads","pictures","trades","professional","fitness","realestate","education","nonprofit","music","personal","landing","aiLayoutKindSignature","kind","random","seed","state","t","Math","imul","pick","next","list","floor","length","aiLayoutFamily","aiLayoutDesignChoices","design","family","signature","hero","group","featureLeft","writing","WORK","WRITING","DISHES","GALLERY","CATEGORIES","aiLayoutGroupVariant","choices","words","test"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-design.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\n/**\n * THE DESIGNER LAYER of the layout compiler (AGL-3660): how a page the model\n * described in the layout language is ARRANGED, decided by code from the\n * site's kind and its job's seed — never asked of the model, so a recorded\n * answer compiles into the new design with no new call.\n *\n * Why. A page compiled one way for every site read like a SaaS landing page:\n * a centered text hero, three outlined cards, a numbered row, a solid form\n * band. The designer templates people compare a site to (Squarespace's Loam,\n * Clove, Auburn, Rivoli, Matsuya; Wix's portfolio and restaurant sets) are\n * built from a handful of moves instead — a full-bleed photo hero with the\n * type over it, image and text side by side, work shown as pictures in an\n * uneven grid, writing as a list under thin rules, a closing band over a\n * photo — and they pick those moves for the KIND of site.\n *\n * So a site's design is a set of VARIANTS — the hero's arrangement, how its\n * groups are drawn, whether its text sections take a picture beside them,\n * how its closing section reads — each chosen among the ones that suit the\n * kind, by the seed. The choices are the site's: every page of one site is\n * drawn with the same hero and the same group style, so the site reads as\n * one design, and two sites of one kind come out differently.\n *\n * This is NOT a template library. A variant is a way of drawing whatever the\n * model wrote; the sections, their order, their bands, their words and how\n * many items each shows are still the model's, page by page.\n *\n * Pure: no tree, no theme, no network.\n */\n\n/** What the compiler is told about the site a page belongs to. */\nexport interface AiLayoutDesign {\n /** The site's kind (`AI_SITE_KINDS`); an unknown one draws as a business site. */\n kind: string\n /** The job's design seed: the same for every page of one site. */\n seed: number\n /** Whether this page is the site's home, which opens tallest. */\n home: boolean\n}\n\n/**\n * How the first section of a page is drawn:\n * - `cover`: a full-bleed photo with the words over it on a scrim, set at\n * the start of the page;\n * - `poster`: a full-bleed photo, taller, the words centered over it in\n * display type (AGL-3985);\n * - `split`: the words beside a tall photo, inside the page's measure;\n * - `bleed`: the band in two halves, a photo filling one edge to edge and\n * the words in the other (AGL-3985);\n * - `editorial`: display type across the page, the lede and the buttons in\n * a row under it, and a wide photo below;\n * - `column`: a masthead — the words centered in a narrow column, a wide\n * photo under them (AGL-3985);\n * - `collage`: the words beside two photos, a tall one and a smaller one\n * laid over its corner (AGL-3985);\n * - `panel`: a full-bleed photo, undimmed, with the words on a card of the\n * page's surface over its lower corner (AGL-3985);\n * - `portrait`: a round photo over the words, all centered: a person's own\n * page (AGL-3985).\n */\nexport type AiLayoutHeroVariant = 'cover' | 'poster' | 'split' | 'bleed' | 'editorial' | 'column' | 'collage' | 'panel' | 'portrait'\n\n/** Every hero arrangement, as {@link AiLayoutHeroVariant} lists them. */\nexport const AI_LAYOUT_HERO_VARIANTS: readonly AiLayoutHeroVariant[] = ['cover', 'poster', 'split', 'bleed', 'editorial', 'column', 'collage', 'panel', 'portrait']\n\n/** The heroes whose words take half the page or less, so their title is the page's title size, never display. */\nexport const AI_LAYOUT_HALF_HEROES: ReadonlySet<AiLayoutHeroVariant> = new Set(['split', 'bleed', 'collage', 'panel'])\n\n/** The heroes that lay their words over a photo, which pins the section to the site's dark scheme. */\nexport const AI_LAYOUT_OVER_PHOTO_HEROES: ReadonlySet<AiLayoutHeroVariant> = new Set(['cover', 'poster'])\n\n/**\n * How a group of cards is drawn where no component draws it:\n * - `cards`: the theme's cards (the compiler's first design);\n * - `ruled`: open columns, each under a thin rule, no card around it;\n * - `pictures`: a picture over each item's title and caption, in an uneven\n * grid (work, products);\n * - `articles`: a picture over each title and summary, in even columns (writing);\n * - `menu`: two columns of lines under rules (dishes, prices left out);\n * - `icons`: open columns, each led by an icon in a soft disc over its title\n * and words — a storefront's reasons to buy (shipping, small batch,\n * returns), never a block of text (AGL-3676).\n */\nexport type AiLayoutGroupVariant = 'cards' | 'ruled' | 'pictures' | 'articles' | 'menu' | 'icons'\n\n/** How a numbered group is drawn: a row of numbers, or a ruled list beside its heading. */\nexport type AiLayoutStepsVariant = 'numbers' | 'timeline'\n\n/** The site-level choices a page is drawn with. */\nexport interface AiLayoutDesignChoices {\n hero: AiLayoutHeroVariant\n /** The group style of a section that names no subject the kind draws its own way. */\n group: 'cards' | 'ruled' | 'icons'\n steps: AiLayoutStepsVariant\n /** Whether a section of words takes a photo beside it. */\n features: boolean\n /** Whether the photo of the first such section sits on the left. */\n featureLeft: boolean\n /** Whether a closing section of words is drawn over a photo. */\n coverClose: boolean\n /** Whether a section's heading sits beside its items rather than over them. */\n splitHeads: boolean\n /** How work is laid out where the kind shows it as pictures. */\n pictures: 'mosaic' | 'even'\n /**\n * How a blog's own posts are listed where a page shows them (AGL-3676):\n * `grid`, a cover over each post's date, title and excerpt in columns; or\n * `ruled`, a list under thin rules, the date in its own column and a small\n * cover beside each title — the two ways an editorial site lists writing.\n */\n writing: 'grid' | 'ruled'\n /**\n * How the titles of a designed group read (AGL-4065): `sentence`, each set\n * in the theme's heading style for an item; or `caps`, each set as the\n * theme's label (its overline: spaced capitals where the site's eyebrows\n * are capitals) over its words, as a designer's ruled columns and icon\n * rows title their items. Only a variant the theme defines is chosen: the\n * case of a section's own heading is the look's font pairing, never the\n * compiler's.\n */\n headingCase: 'sentence' | 'caps'\n /**\n * How the plain and soft bands run down a page (AGL-4065): `alternate`, the\n * bands as the model wrote them except that two neighbors of one never\n * meet — the second takes the other — so sections read apart; or `plain`,\n * one ground for the whole page, every soft band drawn plain and the\n * sections set apart by their spacing. Brand and dark bands are the\n * model's either way.\n */\n bandRhythm: 'alternate' | 'plain'\n}\n\ntype Family = 'gallery' | 'editorial' | 'hospitality' | 'calm' | 'retail' | 'standard'\ntype GroupStyle = AiLayoutDesignChoices['group']\n\nconst FAMILIES: Readonly<Record<string, Family>> = {\n portfolio: 'gallery',\n photography: 'gallery',\n studio: 'gallery',\n blog: 'editorial',\n restaurant: 'hospitality',\n events: 'hospitality',\n yoga: 'calm',\n wellness: 'calm',\n beauty: 'calm',\n store: 'retail',\n}\n\n/**\n * A kind's LAYOUT SIGNATURE (AGL-3985): the arrangements its pages are drawn\n * in, so a café, a law firm and a band never read as one page in three\n * colors. Before it, six kinds shared one list of heroes and every kind drew\n * its groups, its photo-led sections and its headings beside their items at\n * the same odds; the guided start's 21 style previews, which the real\n * compiler draws, read as one page — a header bar, a split or banded hero,\n * one section.\n *\n * - `heroes`: the two first sections the kind opens with, each drawn at even\n * odds (AGL-3899). The first is the one its style card shows. No two kinds\n * share the same two.\n * - `groups`: the group styles a section of items takes where its subject\n * picks none.\n * - `steps`: how its numbered groups read.\n * - `features`: the chance a section of words takes a photo beside it — a\n * café's or a salon's pages are photo-led, a law firm's or a writer's let\n * the type carry them.\n * - `coverClose`: the chance a closing call to action is drawn over a photo.\n * - `splitHeads`: the chance a heading sits beside its items: an\n * editorial, considered page.\n * - `pictures`: how its work is laid out where it shows it as pictures.\n */\nexport interface AiLayoutKindSignature {\n heroes: readonly [AiLayoutHeroVariant, AiLayoutHeroVariant]\n groups: readonly GroupStyle[]\n steps: readonly AiLayoutStepsVariant[]\n features: number\n coverClose: number\n splitHeads: number\n pictures: readonly AiLayoutDesignChoices['pictures'][]\n}\n\nconst BOTH_STEPS: readonly AiLayoutStepsVariant[] = ['timeline', 'numbers']\nconst MOSTLY_MOSAIC: readonly AiLayoutDesignChoices['pictures'][] = ['mosaic', 'mosaic', 'even']\n\n/** Each kind's signature, by its `AI_SITE_KINDS` id. */\nexport const AI_LAYOUT_KIND_SIGNATURES: Readonly<Record<string, AiLayoutKindSignature>> = {\n // Clear and confident: a contained split, or the words on a card over the place.\n business: { heroes: ['split', 'panel'], groups: ['cards', 'icons'], steps: BOTH_STEPS, features: 0.6, coverClose: 0.3, splitHeads: 0.5, pictures: MOSTLY_MOSAIC },\n // Direct: a full-bleed job photo with the promise at the start, services as\n // tight cards, ruled columns or icon rows, and a job's steps numbered or\n // ruled (AGL-4065: two trades sites of 2026-10-25 shared one layout).\n trades: { heroes: ['cover', 'split'], groups: ['cards', 'ruled', 'icons'], steps: BOTH_STEPS, features: 0.5, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Measured: the words on a card over the office, practice areas ruled, headings beside them.\n professional: { heroes: ['panel', 'editorial'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.2, splitHeads: 0.8, pictures: MOSTLY_MOSAIC },\n // Reassuring: half the band a calm room, or a quiet centered masthead.\n wellness: { heroes: ['bleed', 'column'], groups: ['icons', 'cards'], steps: BOTH_STEPS, features: 0.7, coverClose: 0.2, splitHeads: 0.3, pictures: MOSTLY_MOSAIC },\n // Photo-led: the room full-bleed with the name centered over it, the menu as lines.\n restaurant: { heroes: ['poster', 'bleed'], groups: ['ruled', 'cards'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // A storefront: the products themselves, two of them, beside the words.\n store: { heroes: ['collage', 'split'], groups: ['icons'], steps: BOTH_STEPS, features: 1, coverClose: 0.2, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // The work first: display type then a wide picture, or the words beside two pieces.\n portfolio: { heroes: ['editorial', 'collage'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.2, splitHeads: 0.5, pictures: ['mosaic'] },\n // Oversized statements over a wide picture, or full-bleed and dark.\n studio: { heroes: ['editorial', 'cover'], groups: ['ruled', 'cards'], steps: ['timeline'], features: 0.4, coverClose: 0.5, splitHeads: 0.6, pictures: ['mosaic', 'even'] },\n // Pictures first, few words.\n photography: { heroes: ['editorial', 'poster'], groups: ['ruled'], steps: ['timeline'], features: 0.6, coverClose: 0.6, splitHeads: 0.35, pictures: ['even', 'mosaic'] },\n // Reading-first: a centered masthead over the lead picture.\n blog: { heroes: ['column', 'editorial'], groups: ['ruled'], steps: ['timeline'], features: 0.4, coverClose: 0.1, splitHeads: 0.7, pictures: MOSTLY_MOSAIC },\n // Celebratory: two pictures of the day beside its name, or the name over one.\n events: { heroes: ['collage', 'poster'], groups: ['cards', 'icons'], steps: ['timeline'], features: 0.7, coverClose: 0.6, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // High energy: half the band a photo, half a loud headline; or full-bleed.\n // Its groups as cards or ruled columns, its steps numbered or ruled (AGL-4065).\n fitness: { heroes: ['bleed', 'cover'], groups: ['cards', 'ruled'], steps: BOTH_STEPS, features: 0.5, coverClose: 0.7, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Calm and spacious: a centered masthead, or a soft split.\n yoga: { heroes: ['column', 'split'], groups: ['icons', 'cards'], steps: BOTH_STEPS, features: 0.6, coverClose: 0.2, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Image-led: two pictures of the work and the place, or half the band one.\n beauty: { heroes: ['collage', 'bleed'], groups: ['ruled', 'cards'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.3, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Polished: the home full-bleed with the words on a card, or under a scrim.\n realestate: { heroes: ['panel', 'cover'], groups: ['cards', 'ruled'], steps: BOTH_STEPS, features: 0.8, coverClose: 0.5, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Encouraging: a contained split, or the teacher's portrait over the words.\n education: { heroes: ['split', 'portrait'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.6, coverClose: 0.2, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Mission-led: the people full-bleed with the cause over them, or two pictures beside it.\n nonprofit: { heroes: ['cover', 'collage'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.6, coverClose: 0.7, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // Loud: the name huge over a full-bleed photo, at the start or centered.\n music: { heroes: ['cover', 'poster'], groups: ['ruled'], steps: ['timeline'], features: 0.5, coverClose: 0.8, splitHeads: 0.35, pictures: MOSTLY_MOSAIC },\n // A person: their portrait over who they are, or a quiet masthead.\n personal: { heroes: ['portrait', 'column'], groups: ['ruled'], steps: ['timeline'], features: 0.4, coverClose: 0.1, splitHeads: 0.6, pictures: MOSTLY_MOSAIC },\n // One action: the promise centered over the picture of the thing, or over a photo.\n landing: { heroes: ['column', 'poster'], groups: ['icons', 'cards'], steps: ['numbers'], features: 0.5, coverClose: 0.3, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n // Short and striking: one big line over a photo, or on a card over it.\n 'coming-soon': { heroes: ['poster', 'panel'], groups: ['icons', 'ruled'], steps: BOTH_STEPS, features: 0.3, coverClose: 0.2, splitHeads: 0.2, pictures: MOSTLY_MOSAIC },\n}\n\n/** The signature of a kind; an unknown kind draws as a business site. */\nexport function aiLayoutKindSignature(kind: string): AiLayoutKindSignature {\n return AI_LAYOUT_KIND_SIGNATURES[kind] ?? (AI_LAYOUT_KIND_SIGNATURES['business'] as AiLayoutKindSignature)\n}\n\n/** A small deterministic generator over a seed (mulberry32). */\nfunction random(seed: number): () => number {\n let state = seed >>> 0\n return () => {\n state = (state + 0x6d2b79f5) >>> 0\n let t = state\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\nconst pick = <T>(next: () => number, list: readonly T[]): T => list[Math.floor(next() * list.length) % list.length] as T\n\n/** The family a kind's pages are drawn in. */\nexport function aiLayoutFamily(kind: string): Family {\n return FAMILIES[kind] ?? 'standard'\n}\n\n/** The kinds whose item titles are set as labels three sites in ten (AGL-4065); every other kind's one in ten. */\nconst LABEL_TITLE_KINDS: ReadonlySet<string> = new Set(['trades', 'fitness'])\n\n/** The families whose pages keep one ground as often as they alternate: a gallery's, a publication's (AGL-4065). */\nconst ONE_GROUND_FAMILIES: ReadonlySet<Family> = new Set(['gallery', 'editorial'])\n\n/**\n * A site's choices, the same for every page of it, each drawn at even odds\n * among the options its kind's signature lists (AGL-3899, AGL-3985), so two\n * sites of one kind are drawn apart and two kinds never read as one page.\n * Favoring each list's first option made the newest production sites one\n * design: the kind's first hero won 55% before the pick, a business site\n * opened split, groups were ruled two times in three and every text section\n * took a photo.\n */\nexport function aiLayoutDesignChoices(design: AiLayoutDesign): AiLayoutDesignChoices {\n const next = random(design.seed ^ 0x2f6b9d1)\n const family = aiLayoutFamily(design.kind)\n const signature = aiLayoutKindSignature(design.kind)\n const hero = pick(next, signature.heroes)\n const group = pick(next, signature.groups)\n return {\n hero,\n // A store's reasons to buy — the making, the shipping, the care — read\n // as open columns each led by an icon, the way a storefront theme draws\n // them: never a SaaS page's feature cards, and never the plain columns\n // of a title and a sentence the beta.237 Willow Wick home showed four\n // bands of (AGL-3676).\n group: family === 'retail' ? 'icons' : group,\n steps: pick(next, signature.steps),\n // Whether a section of words takes a photo beside it: some sites are\n // photo-led, some let their type carry the page. A storefront never\n // shows a band of text alone (AGL-3676), so its story always takes one.\n features: next() < signature.features || family === 'retail',\n featureLeft: next() < 0.5,\n coverClose: next() < signature.coverClose,\n splitHeads: next() < signature.splitHeads,\n pictures: pick(next, signature.pictures),\n // Drawn last, so every choice above stays what it was for a seed.\n writing: pick(next, ['grid', 'ruled'] as const),\n // Drawn after it (AGL-4065), so a seed's earlier choices stay too.\n headingCase: next() < (LABEL_TITLE_KINDS.has(design.kind) ? 0.3 : 0.1) ? 'caps' : 'sentence',\n bandRhythm: next() < (ONE_GROUND_FAMILIES.has(family) ? 0.5 : 0.35) ? 'plain' : 'alternate',\n }\n}\n\n\nconst WORK = /\\b(work|works|project|projects|portfolio|gallery|galleries|case|cases|studies|selected|commission|commissions|illustration|illustrations|photo|photos|series|book|books|editorial|brand|collection|collections|wedding|weddings|portrait|portraits|session|sessions|shoot|shoots)\\b/i\nconst WRITING = /\\b(article|articles|post|posts|writing|writings|latest|featured|journey|journeys|recipe|recipes|story|stories|essay|essays|guide|guides|issue|issues|episode|episodes)\\b/i\nconst DISHES = /\\b(menu|menus|dish|dishes|breakfast|brunch|lunch|dinner|pastry|pastries|coffee|drink|drinks|cocktail|cocktails|wine|wines|highlight|highlights|favorite|favorites|favourites|special|specials|plate|plates|bake|bakes|bread|cake|cakes)\\b/i\n// A store's own products are listed by its Product grid (AGL-3676), never drawn\n// from words: what a store shows as pictures is how its range is grouped — its\n// collections, gift sets, occasions and scents. \"Candle care and burn notes\" is no band of pictures.\n/**\n * Words a section uses for itself when it is a showing of pictures, on any\n * kind of site (AGL-3923) — \"in pictures\" and \"in photos\" among them\n * (AGL-4065): the 2026-10-25 Rut Runners Racing and Pit Row Tacos homes drew\n * \"The team at the track in pictures\" and \"The trailer and the food in\n * pictures\" as six captions each, with no picture slot for a photo to fill.\n * Whole words only: \"Picture-perfect\" and \"Photoshoot\" show no pictures.\n */\nconst GALLERY = /\\b(gallery|galleries|portfolio|portfolios|lookbook|lookbooks|pictures|photos)\\b/i\nconst CATEGORIES = /\\b(collection|collections|range|ranges|categor(?:y|ies)|gift|gifts|gift sets?|sets|occasion|occasions|scent|scents|edit|edits)\\b/i\n\n/**\n * How a section's group of cards is drawn, from what the section is about:\n * work in a gallery kind and products in a store are pictures, writing in a\n * blog is articles, dishes in a restaurant a menu; anything else takes the\n * site's own group style.\n */\nexport function aiLayoutGroupVariant(\n design: AiLayoutDesign,\n choices: AiLayoutDesignChoices,\n words: string,\n): AiLayoutGroupVariant {\n const family = aiLayoutFamily(design.kind)\n if (family === 'gallery' && (WORK.test(words) || design.kind === 'photography')) return 'pictures'\n if (family === 'editorial' && WRITING.test(words)) return 'articles'\n if (family === 'hospitality' && DISHES.test(words)) return 'menu'\n if (family === 'retail' && CATEGORIES.test(words)) return 'pictures'\n // A section that calls itself a gallery or a portfolio shows pictures on any\n // kind of site (AGL-3923): the beta.265 Kasvora Marketing Solutions home drew\n // its \"Selected work gallery\" as four cards of icons.\n if (GALLERY.test(words)) return 'pictures'\n return choices.group\n}\n"],"names":["AI_LAYOUT_HERO_VARIANTS","AI_LAYOUT_HALF_HEROES","Set","AI_LAYOUT_OVER_PHOTO_HEROES","FAMILIES","portfolio","photography","studio","blog","restaurant","events","yoga","wellness","beauty","store","BOTH_STEPS","MOSTLY_MOSAIC","AI_LAYOUT_KIND_SIGNATURES","business","heroes","groups","steps","features","coverClose","splitHeads","pictures","trades","professional","fitness","realestate","education","nonprofit","music","personal","landing","aiLayoutKindSignature","kind","random","seed","state","t","Math","imul","pick","next","list","floor","length","aiLayoutFamily","LABEL_TITLE_KINDS","ONE_GROUND_FAMILIES","aiLayoutDesignChoices","design","family","signature","hero","group","featureLeft","writing","headingCase","has","bandRhythm","WORK","WRITING","DISHES","GALLERY","CATEGORIES","aiLayoutGroupVariant","choices","words","test"],"mappings":"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"}
@@ -264,10 +264,15 @@ export declare function aiLayoutRecordsPlacements(dataset: {
264
264
  name: string;
265
265
  /** Its records, by name, as {@link groups} files them. */
266
266
  records?: readonly string[];
267
- /** What its records are filed under: the field, by id, and each record's value, by position. */
267
+ /**
268
+ * What its records are filed under: the field, by id, each record's
269
+ * value, by position, and the values its sections list where they were
270
+ * planned for them (AGL-4065).
271
+ */
268
272
  groups?: {
269
273
  field: string;
270
274
  values: readonly string[];
275
+ expected?: readonly string[];
271
276
  } | null;
272
277
  }, screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
273
278
  /**
@@ -293,6 +298,7 @@ export declare function aiLayoutGroupedPlacements(placements: readonly AiLayoutL
293
298
  groups?: {
294
299
  field: string;
295
300
  values: readonly string[];
301
+ expected?: readonly string[];
296
302
  } | null;
297
303
  }): AiLayoutListingPlacement[];
298
304
  /**
@@ -411,14 +411,22 @@ const isHome = (slug)=>firstSegment(slug) === '';
411
411
  * where the page lists the dataset more than once; a section named for no
412
412
  * one group lists them all. Pure.
413
413
  */ export function aiLayoutGroupedPlacements(placements, screen, dataset) {
414
+ var _groups_expected;
414
415
  const groups = dataset.groups;
415
416
  if (placements.length < 2 || !groups || !FIELD_ID.test(groups.field)) return [
416
417
  ...placements
417
418
  ];
419
+ // The values the sections were planned to list come first (AGL-4065): an
420
+ // empty list's sections are filtered too, so the owner's first rows land
421
+ // in the right one.
422
+ const values = [
423
+ ...(_groups_expected = groups.expected) != null ? _groups_expected : [],
424
+ ...groups.values
425
+ ];
418
426
  return placements.map((placement)=>{
419
427
  var _ref, _dataset_records;
420
428
  var _screen_sections_placement_section;
421
- const value = aiLayoutSectionGroup((_ref = (_screen_sections_placement_section = screen.sections[placement.section]) == null ? void 0 : _screen_sections_placement_section.name) != null ? _ref : '', groups.values);
429
+ const value = aiLayoutSectionGroup((_ref = (_screen_sections_placement_section = screen.sections[placement.section]) == null ? void 0 : _screen_sections_placement_section.name) != null ? _ref : '', values);
422
430
  if (!value) return placement;
423
431
  const records = ((_dataset_records = dataset.records) != null ? _dataset_records : []).filter((_name, index)=>{
424
432
  var _groups_values_index;