@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/model/ai-plan-said-sections.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 * A plan's sections of what customers say (AGL-3887).\n *\n * A job never writes a review, a testimonial or a quote for people who did\n * not say it (rule 14, `AI_SITE_CONTEXT_RULE`). So a section planned to show\n * them, on a site that has none, has nothing true to hold, and the build\n * fills it with the only honest thing left: a promise of words to come. The\n * live Whitmore Lane Family Law start (beta.262, 2026-10-16) planned \"Client\n * feedback placeholder\" on its home page and shipped \"We are building this\n * space for the words of families we have worked with\" over a stock photo.\n *\n * A site start's plan is settled in code before it is kept, never re-asked\n * (a re-ask costs the person credits for a plan that has one answer): such a\n * section becomes a band about what working together is like, as a\n * photographer's start of the same day planned its \"Kind words\" as \"What\n * working together is like\" (Relaxed / Prepared / Personal). Where the page\n * already has that band, the section goes, if the page keeps its fewest\n * sections without it.\n *\n * Kept as planned where the customer words are real: a store, whose\n * platform shows its shoppers' reviews; a site with a dataset of them; a\n * brief that asks for them or quotes them.\n *\n * Every plan, whatever its kind, names no section \"placeholder\": a section's\n * name is what its build is asked to write, and a name that says it is a\n * stand-in is written as one.\n *\n * Pure: plans in, plans out.\n */\n\nimport type { AiBuildPlan, AiBuildPlanScreen, AiBuildPlanSection } from './ai-build-plan'\nimport type { AiSiteInventory } from './ai-site-inventory'\n\n/** Who a section of customer words quotes. */\nconst SAYERS =\n '(?:clients?|customers?|patients?|guests?|famil(?:y|ies)|parents?|students?|readers?|members?|couples?|homeowners?|buyers?|shoppers?|diners?|people|others)'\n\n/**\n * The words a section of what customers say is named by. \"Review\" alone is\n * not one of them: a law firm's \"Free case review\" or an accountant's \"Plan\n * review\" is a service, not a quote.\n */\nconst SAID_SECTION = new RegExp(\n [\n '\\\\btestimonials?\\\\b',\n '\\\\bkind words\\\\b',\n '\\\\bsocial proof\\\\b',\n '\\\\bcustomer love\\\\b',\n '\\\\bendorsements?\\\\b',\n '\\\\bin their (?:own )?words\\\\b',\n '\\\\bword of mouth\\\\b',\n '\\\\bsuccess stories\\\\b',\n `\\\\b${SAYERS}['’]?s?['’]? (?:feedback|reviews?|stories|voices|words|praise|say(?:s|ing)?)\\\\b`,\n `\\\\bwhat (?:our |my |real )?(?:\\\\w+ )?${SAYERS} (?:say|are saying|think|tell us)\\\\b`,\n '\\\\b(?:star|5-star|five-star|google|yelp|online|verified) (?:reviews?|ratings?)\\\\b',\n '\\\\bratings? (?:and|&) reviews?\\\\b',\n '\\\\breviews? (?:and|&) (?:ratings?|testimonials?)\\\\b',\n '^\\\\s*(?:reviews|feedback)\\\\s*$',\n ].join('|'),\n 'i',\n)\n\n/** A dataset's name that says it holds what customers said. */\nconst SAID_RECORDS = /\\b(?:reviews|testimonials?|kind words|endorsements?)\\b/i\n\n/** A quotation with its speaker: “…” — Name, the way a brief gives a real testimonial. */\nconst ATTRIBUTED_QUOTE = /[“\"][^”\"]{12,}[”\"]\\s*[—–-]+\\s*\\p{L}/u\n\n/** A section already about working together: its approach, its process, what to expect. */\nconst APPROACH =\n /\\b(?:what (?:working|it(?:['’]s| is) like)|working (?:together|with (?:us|me|our team))|how (?:we|i|it) work|our (?:approach|process)|the process|approach|process|what to expect)\\b/i\n\n/** The word a stand-in is named by, with any brackets, parentheses or dash it is set off by. */\nconst PLACEHOLDER_WORD = /\\s*[[(]?\\s*[-–—:]?\\s*\\bplace ?holders?\\b\\s*[\\])]?/gi\n\n/** What a section of customer words on a site that has none is planned as instead. */\nexport const AI_PLAN_SAID_SECTION_NAME = 'What working with us is like'\n\n/** The points that band makes (the photographer's three: Relaxed, Prepared, Personal). */\nexport const AI_PLAN_SAID_SECTION_ITEMS = 3\n\n/** The fewest sections any page other than the home keeps after a section goes. */\nconst PAGE_MIN_SECTIONS = 2\n\n/** Whether a planned section's name says it shows what customers say. */\nexport function aiPlanSectionSaysCustomerWords(name: string): boolean {\n return SAID_SECTION.test(aiPlanSectionNameWithoutPlaceholder(name))\n}\n\n/** A section's name with the word \"placeholder\" taken out; the name unchanged where nothing is left. */\nexport function aiPlanSectionNameWithoutPlaceholder(name: string): string {\n if (!/\\bplace ?holders?\\b/i.test(name)) return name\n const cleaned = name\n .replace(PLACEHOLDER_WORD, ' ')\n .replace(/\\s{2,}/g, ' ')\n .replace(/^[\\s\\-–—:,]+|[\\s\\-–—:,]+$/g, '')\n .trim()\n return cleaned || 'Section'\n}\n\n/**\n * Whether the site, or the person asking, has customer words that are real\n * (AGL-3887): a store's shoppers' reviews, which its platform shows; a\n * dataset of them the site keeps; a brief that names them or quotes one with\n * its speaker.\n */\nexport function aiPlanHasCustomerWords(input: {\n brief?: string | null\n inventory?: Pick<AiSiteInventory, 'datasets'> | null\n siteKind?: string | null\n}): boolean {\n if (input.siteKind === 'store') return true\n if ((input.inventory?.datasets ?? []).some((dataset) => SAID_RECORDS.test(dataset.name))) return true\n const brief = input.brief ?? ''\n return SAID_SECTION.test(brief) || ATTRIBUTED_QUOTE.test(brief)\n}\n\n/** Every section's name with the word \"placeholder\" taken out (AGL-3887), the plan unchanged where none had it. */\nexport function aiSettlePlanPlaceholderNames<P extends Pick<AiBuildPlan, 'screens'>>(plan: P): P {\n let changed = false\n const screens = plan.screens.map((screen) => {\n let touched = false\n const sections = screen.sections.map((section) => {\n const name = aiPlanSectionNameWithoutPlaceholder(section.name)\n if (name === section.name) return section\n touched = true\n return { ...section, name }\n })\n if (!touched) return screen\n changed = true\n return { ...screen, sections }\n })\n return changed ? { ...plan, screens } : plan\n}\n\n/**\n * A site start's plan with no section of customer words it cannot fill\n * (AGL-3887). Where `hasCustomerWords` is false, each such section past a\n * page's first is planned as {@link AI_PLAN_SAID_SECTION_NAME} with its\n * points; where the page already has that band, it is taken out instead\n * while the page keeps its fewest sections (`homeMin` on the home page, two\n * on any other), and planned as the band all the same where it cannot be.\n * The plan is returned unchanged where nothing moved.\n */\nexport function aiSettlePlanSaidSections<P extends Pick<AiBuildPlan, 'screens'>>(\n plan: P,\n options: { hasCustomerWords: boolean; homeMin: number },\n): P {\n if (options.hasCustomerWords) return plan\n let changed = false\n const screens = plan.screens.map((screen) => {\n const next = settleScreen(screen, options.homeMin)\n if (next !== screen) changed = true\n return next\n })\n return changed ? { ...plan, screens } : plan\n}\n\nfunction isHome(screen: Pick<AiBuildPlanScreen, 'slug'>): boolean {\n return screen.slug.trim().replace(/^\\/+|\\/+$/g, '') === ''\n}\n\nfunction settleScreen<S extends Pick<AiBuildPlanScreen, 'slug' | 'sections'>>(screen: S, homeMin: number): S {\n const said = screen.sections.flatMap((section, index) =>\n index > 0 && aiPlanSectionSaysCustomerWords(section.name) ? [index] : [],\n )\n if (!said.length) return screen\n const floor = isHome(screen) ? Math.max(homeMin, PAGE_MIN_SECTIONS) : PAGE_MIN_SECTIONS\n let approach = screen.sections.some((section, index) => !said.includes(index) && APPROACH.test(section.name))\n let count = screen.sections.length\n const sections: AiBuildPlanSection[] = []\n screen.sections.forEach((section, index) => {\n if (!said.includes(index)) {\n sections.push(section)\n return\n }\n if (approach && count - 1 >= floor) {\n count -= 1\n return\n }\n approach = true\n sections.push({\n ...section,\n name: AI_PLAN_SAID_SECTION_NAME,\n items: Math.max(section.items, AI_PLAN_SAID_SECTION_ITEMS),\n })\n })\n return { ...screen, sections }\n}\n"],"names":["SAYERS","SAID_SECTION","RegExp","join","SAID_RECORDS","ATTRIBUTED_QUOTE","APPROACH","PLACEHOLDER_WORD","AI_PLAN_SAID_SECTION_NAME","AI_PLAN_SAID_SECTION_ITEMS","PAGE_MIN_SECTIONS","aiPlanSectionSaysCustomerWords","name","test","aiPlanSectionNameWithoutPlaceholder","cleaned","replace","trim","aiPlanHasCustomerWords","input","siteKind","inventory","datasets","some","dataset","brief","aiSettlePlanPlaceholderNames","plan","changed","screens","map","screen","touched","sections","section","aiSettlePlanSaidSections","options","hasCustomerWords","next","settleScreen","homeMin","isHome","slug","said","flatMap","index","length","floor","Math","max","approach","includes","count","forEach","push","items"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-said-sections.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 * A plan's sections of what customers say (AGL-3887).\n *\n * A job never writes a review, a testimonial or a quote for people who did\n * not say it (rule 14, `AI_SITE_CONTEXT_RULE`). So a section planned to show\n * them, on a site that has none, has nothing true to hold, and the build\n * fills it with the only honest thing left: a promise of words to come. The\n * live Whitmore Lane Family Law start (beta.262, 2026-10-16) planned \"Client\n * feedback placeholder\" on its home page and shipped \"We are building this\n * space for the words of families we have worked with\" over a stock photo.\n *\n * A site start's plan is settled in code before it is kept, never re-asked\n * (a re-ask costs the person credits for a plan that has one answer): such a\n * section becomes a band about what working together is like, as a\n * photographer's start of the same day planned its \"Kind words\" as \"What\n * working together is like\" (Relaxed / Prepared / Personal). Where the page\n * already has that band, the section goes, if the page keeps its fewest\n * sections without it.\n *\n * Kept as planned where the customer words are real: a store, whose\n * platform shows its shoppers' reviews; a site with a dataset of them; a\n * brief that asks for them or quotes them.\n *\n * Every plan, whatever its kind, names no section \"placeholder\": a section's\n * name is what its build is asked to write, and a name that says it is a\n * stand-in is written as one. Nor does it name a gap (AGL-4092): an Assist\n * build's About plan of 2026-10-28 named \"Who we are: …, with [year\n * founded] and [team story]\" and \"Contact details: [email], [phone], …\", and\n * its plan card printed them. A fact in square brackets, \"to be added\" and\n * \"TBD\" are taken out of a name, with the words that joined them to it.\n *\n * Pure: plans in, plans out.\n */\n\nimport type { AiBuildPlan, AiBuildPlanScreen, AiBuildPlanSection } from './ai-build-plan'\nimport type { AiSiteInventory } from './ai-site-inventory'\n\n/** Who a section of customer words quotes. */\nconst SAYERS =\n '(?:clients?|customers?|patients?|guests?|famil(?:y|ies)|parents?|students?|readers?|members?|couples?|homeowners?|buyers?|shoppers?|diners?|people|others)'\n\n/**\n * The words a section of what customers say is named by. \"Review\" alone is\n * not one of them: a law firm's \"Free case review\" or an accountant's \"Plan\n * review\" is a service, not a quote.\n */\nconst SAID_SECTION = new RegExp(\n [\n '\\\\btestimonials?\\\\b',\n '\\\\bkind words\\\\b',\n '\\\\bsocial proof\\\\b',\n '\\\\bcustomer love\\\\b',\n '\\\\bendorsements?\\\\b',\n '\\\\bin their (?:own )?words\\\\b',\n '\\\\bword of mouth\\\\b',\n '\\\\bsuccess stories\\\\b',\n `\\\\b${SAYERS}['’]?s?['’]? (?:feedback|reviews?|stories|voices|words|praise|say(?:s|ing)?)\\\\b`,\n `\\\\bwhat (?:our |my |real )?(?:\\\\w+ )?${SAYERS} (?:say|are saying|think|tell us)\\\\b`,\n '\\\\b(?:star|5-star|five-star|google|yelp|online|verified) (?:reviews?|ratings?)\\\\b',\n '\\\\bratings? (?:and|&) reviews?\\\\b',\n '\\\\breviews? (?:and|&) (?:ratings?|testimonials?)\\\\b',\n '^\\\\s*(?:reviews|feedback)\\\\s*$',\n ].join('|'),\n 'i',\n)\n\n/** A dataset's name that says it holds what customers said. */\nconst SAID_RECORDS = /\\b(?:reviews|testimonials?|kind words|endorsements?)\\b/i\n\n/** A quotation with its speaker: “…” — Name, the way a brief gives a real testimonial. */\nconst ATTRIBUTED_QUOTE = /[“\"][^”\"]{12,}[”\"]\\s*[—–-]+\\s*\\p{L}/u\n\n/** A section already about working together: its approach, its process, what to expect. */\nconst APPROACH =\n /\\b(?:what (?:working|it(?:['’]s| is) like)|working (?:together|with (?:us|me|our team))|how (?:we|i|it) work|our (?:approach|process)|the process|approach|process|what to expect)\\b/i\n\n/** The word a stand-in is named by, with any brackets, parentheses or dash it is set off by. */\nconst PLACEHOLDER_WORD = /\\s*[[(]?\\s*[-–—:]?\\s*\\bplace ?holders?\\b\\s*[\\])]?/gi\n\n/** What a section of customer words on a site that has none is planned as instead. */\nexport const AI_PLAN_SAID_SECTION_NAME = 'What working with us is like'\n\n/** The points that band makes (the photographer's three: Relaxed, Prepared, Personal). */\nexport const AI_PLAN_SAID_SECTION_ITEMS = 3\n\n/** The fewest sections any page other than the home keeps after a section goes. */\nconst PAGE_MIN_SECTIONS = 2\n\n/** Whether a planned section's name says it shows what customers say. */\nexport function aiPlanSectionSaysCustomerWords(name: string): boolean {\n return SAID_SECTION.test(aiPlanSectionNameWithoutPlaceholder(name))\n}\n\n/** A fact a plan left in square brackets, as the doctrine's validators read one (`BRACKETED_FACT`): \"[year founded]\". */\nconst BRACKETED_GAP = /\\[[^[\\]{}\\n]{1,80}\\]/g\n\n/** A gap's own words, \"to be added\" or \"TBD\", with any parentheses or dash they are set off by. */\nconst GAP_WORDS =\n /\\s*[(]?\\s*[-–—:]?\\s*\\b(?:(?:details?|dates?|info|information|names?)\\s+)?(?:to be (?:added|confirmed|announced|determined|decided)|tbd|tba)\\b\\s*[)]?/gi\n\n/** Where a bracketed gap stood while the words around it are tidied. */\nconst GAP = '\\uE000'\n\n/** Words that join a gap to a list: a comma, \"and\", \"or\". */\nconst LIST_JOIN = '\\\\s*(?:,|;|&|/|\\\\band\\\\b|\\\\bor\\\\b)(?:\\\\s*(?:\\\\band\\\\b|\\\\bor\\\\b))?\\\\s*'\n\n/** A run of gaps listed together: \"[year founded] and [team story]\", \"[email], [phone]\". */\nconst GAP_RUN = new RegExp(`${GAP}(?:${LIST_JOIN}${GAP})+`, 'g')\n\n/** A gap that opens a name or a list after a colon, and the comma after it: \"Contact details: [email], …\". */\nconst LEADING_GAP = new RegExp(`(^|:\\\\s*)${GAP}(?:\\\\s*,\\\\s*(?:(?:and|or)\\\\s+)?)?`, 'g')\n\n/** A gap and the words that joined it to the name: \", with [year founded]\", \", and [age and class requirements]\". */\nconst JOINED_GAP = new RegExp(\n `\\\\s*,?\\\\s*(?:\\\\b(?:and|or|with|including|plus|such as|like|in|since|on|at|from|by)\\\\b\\\\s*|&\\\\s*)*${GAP}`,\n 'gi',\n)\n\n/** Whether a name says it is a stand-in, or holds a gap (AGL-3887, AGL-4092). */\nconst HOLDS_GAP = /\\bplace ?holders?\\b|\\[[^[\\]{}\\n]{1,80}\\]|\\bto be (?:added|confirmed|announced|determined|decided)\\b|\\b(?:tbd|tba)\\b/i\n\n/**\n * A section's name with the word \"placeholder\" (AGL-3887) and every gap\n * (AGL-4092) taken out, with the words that joined them: \"Who we are: a\n * team, with [year founded] and [team story]\" is \"Who we are: a team\". The\n * name unchanged where it had none; \"Section\" where nothing is left.\n */\nexport function aiPlanSectionNameWithoutPlaceholder(name: string): string {\n if (!HOLDS_GAP.test(name)) return name\n const cleaned = name\n .replace(PLACEHOLDER_WORD, ' ')\n .replace(GAP_WORDS, ' ')\n .replace(BRACKETED_GAP, GAP)\n .replace(GAP_RUN, GAP)\n .replace(LEADING_GAP, '$1')\n .replace(JOINED_GAP, '')\n .split(GAP)\n .join('')\n .replace(/\\(\\s*\\)/g, '')\n .replace(/\\s+,/g, ',')\n .replace(/,(?:\\s*,)+/g, ',')\n .replace(/\\s{2,}/g, ' ')\n .replace(/^[\\s\\-–—:,;]+|[\\s\\-–—:,;]+$/g, '')\n .trim()\n return cleaned || 'Section'\n}\n\n/**\n * Whether the site, or the person asking, has customer words that are real\n * (AGL-3887): a store's shoppers' reviews, which its platform shows; a\n * dataset of them the site keeps; a brief that names them or quotes one with\n * its speaker.\n */\nexport function aiPlanHasCustomerWords(input: {\n brief?: string | null\n inventory?: Pick<AiSiteInventory, 'datasets'> | null\n siteKind?: string | null\n}): boolean {\n if (input.siteKind === 'store') return true\n if ((input.inventory?.datasets ?? []).some((dataset) => SAID_RECORDS.test(dataset.name))) return true\n const brief = input.brief ?? ''\n return SAID_SECTION.test(brief) || ATTRIBUTED_QUOTE.test(brief)\n}\n\n/** Every section's name with the word \"placeholder\" (AGL-3887) and every gap (AGL-4092) taken out, the plan unchanged where none had one. */\nexport function aiSettlePlanPlaceholderNames<P extends Pick<AiBuildPlan, 'screens'>>(plan: P): P {\n let changed = false\n const screens = plan.screens.map((screen) => {\n let touched = false\n const sections = screen.sections.map((section) => {\n const name = aiPlanSectionNameWithoutPlaceholder(section.name)\n if (name === section.name) return section\n touched = true\n return { ...section, name }\n })\n if (!touched) return screen\n changed = true\n return { ...screen, sections }\n })\n return changed ? { ...plan, screens } : plan\n}\n\n/**\n * A site start's plan with no section of customer words it cannot fill\n * (AGL-3887). Where `hasCustomerWords` is false, each such section past a\n * page's first is planned as {@link AI_PLAN_SAID_SECTION_NAME} with its\n * points; where the page already has that band, it is taken out instead\n * while the page keeps its fewest sections (`homeMin` on the home page, two\n * on any other), and planned as the band all the same where it cannot be.\n * The plan is returned unchanged where nothing moved.\n */\nexport function aiSettlePlanSaidSections<P extends Pick<AiBuildPlan, 'screens'>>(\n plan: P,\n options: { hasCustomerWords: boolean; homeMin: number },\n): P {\n if (options.hasCustomerWords) return plan\n let changed = false\n const screens = plan.screens.map((screen) => {\n const next = settleScreen(screen, options.homeMin)\n if (next !== screen) changed = true\n return next\n })\n return changed ? { ...plan, screens } : plan\n}\n\nfunction isHome(screen: Pick<AiBuildPlanScreen, 'slug'>): boolean {\n return screen.slug.trim().replace(/^\\/+|\\/+$/g, '') === ''\n}\n\nfunction settleScreen<S extends Pick<AiBuildPlanScreen, 'slug' | 'sections'>>(screen: S, homeMin: number): S {\n const said = screen.sections.flatMap((section, index) =>\n index > 0 && aiPlanSectionSaysCustomerWords(section.name) ? [index] : [],\n )\n if (!said.length) return screen\n const floor = isHome(screen) ? Math.max(homeMin, PAGE_MIN_SECTIONS) : PAGE_MIN_SECTIONS\n let approach = screen.sections.some((section, index) => !said.includes(index) && APPROACH.test(section.name))\n let count = screen.sections.length\n const sections: AiBuildPlanSection[] = []\n screen.sections.forEach((section, index) => {\n if (!said.includes(index)) {\n sections.push(section)\n return\n }\n if (approach && count - 1 >= floor) {\n count -= 1\n return\n }\n approach = true\n sections.push({\n ...section,\n name: AI_PLAN_SAID_SECTION_NAME,\n items: Math.max(section.items, AI_PLAN_SAID_SECTION_ITEMS),\n })\n })\n return { ...screen, sections }\n}\n"],"names":["SAYERS","SAID_SECTION","RegExp","join","SAID_RECORDS","ATTRIBUTED_QUOTE","APPROACH","PLACEHOLDER_WORD","AI_PLAN_SAID_SECTION_NAME","AI_PLAN_SAID_SECTION_ITEMS","PAGE_MIN_SECTIONS","aiPlanSectionSaysCustomerWords","name","test","aiPlanSectionNameWithoutPlaceholder","BRACKETED_GAP","GAP_WORDS","GAP","LIST_JOIN","GAP_RUN","LEADING_GAP","JOINED_GAP","HOLDS_GAP","cleaned","replace","split","trim","aiPlanHasCustomerWords","input","siteKind","inventory","datasets","some","dataset","brief","aiSettlePlanPlaceholderNames","plan","changed","screens","map","screen","touched","sections","section","aiSettlePlanSaidSections","options","hasCustomerWords","next","settleScreen","homeMin","isHome","slug","said","flatMap","index","length","floor","Math","max","approach","includes","count","forEach","push","items"],"mappings":"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"}
@@ -233,6 +233,24 @@ export interface AiCatalogProposal {
233
233
  products: AiCatalogProduct[];
234
234
  notes: string[];
235
235
  }
236
+ /**
237
+ * The re-ask a catalog whose names are kinds of product is answered with
238
+ * (AGL-4065). The 2026-10-25 Berm & Bolt Moto Parts start listed "Riding Gear
239
+ * Item", "OEM ATV Parts Order" and "Aftermarket Dirt Bike Part" at suggested
240
+ * prices, from a brief that named kinds of part and no product.
241
+ */
242
+ export declare const AI_CATALOG_NAME_REASK = "Name real products a shopper would buy, never a category with \u201CItem\u201D or \u201COrder\u201D after it; where the brief names kinds of product and no product, name each one a plain item of one of those kinds, never a brand or a model the brief does not name.";
243
+ /** The code a catalog's kind-for-a-name is refused under. */
244
+ export declare const AI_CATALOG_NAME_CODE = "catalog-name-kind";
245
+ /** The make a text names that the brief does not (AGL-4065), or `null`: a sample row or a product names none. Pure. */
246
+ export declare function aiTextNamesBrand(text: string, brief: string): string | null;
247
+ /**
248
+ * Why a proposed product's name is not one a shopper would buy (AGL-4065), or
249
+ * `null`: a kind of product with a word like "Item", "Order" or "Part" after
250
+ * it, or a make or a model number the brief does not name. "Dirt Bike Tire"
251
+ * and "Motocross helmet" are products; "Riding Gear Item" is a kind. Pure.
252
+ */
253
+ export declare function aiCatalogNameProblem(name: string, brief: string): string | null;
236
254
  export type AiDiscountKind = 'percent' | 'fixed' | 'free_shipping';
237
255
  export interface AiProposedDiscount {
238
256
  name: string;
@@ -236,6 +236,38 @@ export const AI_PRODUCTS_TOO_MANY_COPY = `Pick at most ${AI_PRODUCTS_BULK_MAX} p
236
236
  target: target
237
237
  };
238
238
  }
239
+ // ── A product a shopper would buy (AGL-4065) ────────────────────────────
240
+ /**
241
+ * The re-ask a catalog whose names are kinds of product is answered with
242
+ * (AGL-4065). The 2026-10-25 Berm & Bolt Moto Parts start listed "Riding Gear
243
+ * Item", "OEM ATV Parts Order" and "Aftermarket Dirt Bike Part" at suggested
244
+ * prices, from a brief that named kinds of part and no product.
245
+ */ export const AI_CATALOG_NAME_REASK = 'Name real products a shopper would buy, never a category with “Item” or “Order” after it; where the brief names kinds of product and no product, name each one a plain item of one of those kinds, never a brand or a model the brief does not name.';
246
+ /** The code a catalog's kind-for-a-name is refused under. */ export const AI_CATALOG_NAME_CODE = 'catalog-name-kind';
247
+ /** A name's last word that names a kind of thing, or a transaction, never one thing a shopper buys. */ const KIND_HEAD = /\b(?:items?|orders?|products?|parts?|things?|options?|selections?|assortments?|stuff|goods|merchandise|gear|accessor(?:y|ies)|suppl(?:y|ies)|equipment|essentials?|requests?|inquir(?:y|ies))$/i;
248
+ /** Makes a product name carries only where the brief names them: vehicles, apparel and electronics most of all. */ const BRANDS = /\b(?:porsche|ferrari|lamborghini|bmw|mercedes|audi|volkswagen|toyota|honda|yamaha|kawasaki|suzuki|ktm|ducati|harley(?:-davidson)?|husqvarna|gasgas|polaris|can-am|tesla|ford|chevrolet|chevy|dodge|jeep|nissan|subaru|mazda|nike|adidas|reebok|samsung|iphone|playstation|xbox|nintendo|starbucks|coca-cola|pepsi)\b/i;
249
+ /** A model number: letters and figures run together, as "CRF450R" or "KX250". */ const MODEL_CODE = /\b(?=[A-Z0-9-]*\d)(?=[A-Z0-9-]*[A-Z])[A-Z0-9][A-Z0-9-]{3,}\b/;
250
+ /** The make a text names that the brief does not (AGL-4065), or `null`: a sample row or a product names none. Pure. */ export function aiTextNamesBrand(text, brief) {
251
+ var _BRANDS_exec;
252
+ const brand = (_BRANDS_exec = BRANDS.exec(text)) == null ? void 0 : _BRANDS_exec[0];
253
+ return brand && !brief.toLowerCase().includes(brand.toLowerCase()) ? brand : null;
254
+ }
255
+ /**
256
+ * Why a proposed product's name is not one a shopper would buy (AGL-4065), or
257
+ * `null`: a kind of product with a word like "Item", "Order" or "Part" after
258
+ * it, or a make or a model number the brief does not name. "Dirt Bike Tire"
259
+ * and "Motocross helmet" are products; "Riding Gear Item" is a kind. Pure.
260
+ */ export function aiCatalogNameProblem(name, brief) {
261
+ var _MODEL_CODE_exec;
262
+ const plain = name.replace(/\s+/g, ' ').trim();
263
+ if (!plain) return null;
264
+ const words = brief.toLowerCase();
265
+ if (KIND_HEAD.test(plain.replace(RegExp("[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+$", "u"), ''))) return `“${plain}” names a kind of product, not one a shopper buys`;
266
+ if (aiTextNamesBrand(plain, brief)) return `“${plain}” names a make the brief does not`;
267
+ const model = (_MODEL_CODE_exec = MODEL_CODE.exec(plain)) == null ? void 0 : _MODEL_CODE_exec[0];
268
+ if (model && !words.includes(model.toLowerCase())) return `“${plain}” names a model the brief does not`;
269
+ return null;
270
+ }
239
271
  /** Output ids, so a pass that runs again finds what it already proposed. */ export const AI_PRODUCTS_OUTPUT_IDS = {
240
272
  copy: (productId)=>`copy:${productId != null ? productId : 'new'}`,
241
273
  catalog: 'catalog',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-products.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\nimport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief, aiProductsBulkInputs } from './ai-products-bulk'\n\nexport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief }\n\n/**\n * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it\n * proposes. Pure, so the step that asks, the console cards that start a job\n * and show its proposals, and the eval harness all read one shape.\n *\n * `inputs.target` names the work:\n *\n * - `product` — one product's copy, from what its editor holds right now:\n * the name, the text, the tags, the categories and options it has, and the\n * first photo. The editor may hold a product nobody has saved yet, so the\n * job never reads a product document for this target.\n * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one\n * product a pass, each read from the catalog as it is reached.\n * - `catalog` — a store's first products from the brief: names, copy, tags\n * and options, what each photo should show, and a suggested price by what\n * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.\n * - `categories` — store categories and a first set of discounts from the brief.\n *\n * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the\n * job writes nothing. The commerce plugin's own surfaces write what a person\n * accepts, through the doors those surfaces already use.\n */\n\nexport const AI_PRODUCTS_TARGETS = ['product', 'bulk', 'catalog', 'categories'] as const\nexport type AiProductsTarget = (typeof AI_PRODUCTS_TARGETS)[number]\n\nexport const AI_PRODUCT_TYPES = ['physical', 'digital', 'service'] as const\nexport type AiProductType = (typeof AI_PRODUCT_TYPES)[number]\n\n/** The longest product name the editor saves. */\nexport const AI_PRODUCT_NAME_MAX_CHARS = 120\n\n/** How much of a product's existing description a request carries. */\nexport const AI_PRODUCT_TEXT_MAX_CHARS = 2_000\n\n/** The longest description the job proposes. */\nexport const AI_PRODUCT_DESCRIPTION_MAX_CHARS = 1_200\n\nexport const AI_PRODUCT_TAGS_MAX = 8\nexport const AI_PRODUCT_TAG_MAX_CHARS = 30\n\n/** The most categories one product's proposal names. */\nexport const AI_PRODUCT_CATEGORIES_MAX = 3\n\n/** The most of a site's categories a request lists to choose from. */\nexport const AI_PRODUCT_CATEGORY_LIST_MAX = 100\n\n/** A product's option axes, as the commerce editor bounds them. */\nexport const AI_PRODUCT_OPTIONS_MAX = 3\nexport const AI_PRODUCT_OPTION_VALUES_MAX = 25\nexport const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30\n\n/** How many products a catalog proposal holds. */\nexport const AI_CATALOG_PRODUCTS = { min: 1, max: 12 } as const\n\n/**\n * A proposed product is shorter than a written one: its description and tags\n * are a start the owner completes, and twelve of them at every bound still fit\n * one answer (`ai-job-products-step.spec.ts` measures it).\n */\nexport const AI_CATALOG_DESCRIPTION_MAX_CHARS = 600\nexport const AI_CATALOG_TAGS_MAX = 6\n\n/** A proposed product's options: few axes and few short values, so its variants stay a short list. */\nexport const AI_CATALOG_OPTIONS_MAX = 2\nexport const AI_CATALOG_OPTION_VALUES_MAX = 6\nexport const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30\n\n/** What a proposed product's photo should show, in one sentence. */\nexport const AI_CATALOG_PHOTO_MAX_CHARS = 160\n\n/**\n * A proposed product's suggested price, in US dollars — the one currency a\n * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,\n * 2026-10-12): a store's first products priced by what each one is (a bar\n * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead\n * of every one at the commerce default. Never $0 or less, nothing past what\n * a small shop's first product plausibly lists at; a price outside the range,\n * or none, is no price, and the writer gives the product its default price.\n * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.\n */\nexport const AI_CATALOG_PRICE_USD = { min: 1, max: 5_000 } as const\n\n/**\n * A proposed price as code keeps it: a finite number of dollars (or a plain\n * \"$12.50\" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to\n * whole cents; anything else — missing, zero, negative, absurd, not a number\n * — is `null`, so the product takes the store's default price for the owner\n * to change. The model is never re-asked for a price: a bad one costs the\n * product only its suggestion.\n */\nexport function aiCatalogPrice(raw: unknown): number | null {\n let value: number | null = null\n if (typeof raw === 'number') value = raw\n else if (typeof raw === 'string') {\n const match = /^\\s*(?:US\\s*)?\\$?\\s*(\\d{1,6}(?:\\.\\d{1,2})?)\\s*(?:USD)?\\s*$/i.exec(raw)\n value = match ? Number(match[1]) : null\n }\n if (value === null || !Number.isFinite(value)) return null\n const cents = Math.round(value * 100)\n if (cents < AI_CATALOG_PRICE_USD.min * 100 || cents > AI_CATALOG_PRICE_USD.max * 100) return null\n return cents / 100\n}\n\nexport const AI_CATEGORIES_MAX = 12\nexport const AI_DISCOUNTS_MAX = 5\nexport const AI_CATEGORY_NAME_MAX_CHARS = 60\nexport const AI_PROPOSAL_WHY_MAX_CHARS = 200\n\n/** The highest percentage a proposed discount takes off, and the most a fixed one takes, in dollars. */\nexport const AI_DISCOUNT_MAX_PERCENT = 90\nexport const AI_DISCOUNT_MAX_FIXED_USD = 1_000\n\n/** A discount code: letters, digits, dashes and underscores, stored upper case. */\nexport const AI_DISCOUNT_CODE = /^[A-Z0-9_-]{3,20}$/\n\n/** An option axis. */\nexport interface AiProductOption {\n name: string\n values: string[]\n}\n\n/** One product as its editor hands it to a `product` job. */\nexport interface AiProductFacts {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: AiProductType\n /** The product's current description, cut to `AI_PRODUCT_TEXT_MAX_CHARS`. */\n text: string\n tags: string[]\n categoryIds: string[]\n options: AiProductOption[]\n /** The first photo's stored media value, or `null`. */\n imageUrl: string | null\n seoTitle: string\n seoDescription: string\n}\n\n/** A site's product category, as a request lists it to choose from. */\nexport interface AiProductCategory {\n id: string\n name: string\n}\n\n/** The merchant's own words about a product, which a claim in its copy may repeat. */\nexport function aiProductMerchantWords(product: Pick<AiProductFacts, 'name' | 'text' | 'tags'>): string {\n return [product.name, product.text, ...product.tags].join('\\n')\n}\n\nexport type AiProductsJobInputs =\n | { target: 'product'; product: AiProductFacts }\n | { target: 'bulk'; productIds: string[] }\n | { target: 'catalog' }\n | { target: 'categories' }\n\nconst ID = /^[A-Za-z0-9_-]{1,64}$/\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\n/** A list stored as JSON, read defensively: strings only, trimmed, bounded. */\nfunction jsonStrings(value: unknown, max: number, maxChars: number): string[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => text(entry, maxChars))\n .filter(Boolean)\n .slice(0, max)\n } catch {\n return []\n }\n}\n\nfunction jsonOptions(value: unknown): AiProductOption[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => {\n const record = (entry ?? {}) as Record<string, unknown>\n return {\n name: text(record['name'], AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: Array.isArray(record['values'])\n ? record['values'].map((value) => text(value, 40)).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)\n : [],\n }\n })\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n } catch {\n return []\n }\n}\n\n/**\n * How large a job's inputs may be once written as JSON: under the 8,000\n * characters the create door admits, with room for the door's own keys.\n */\nexport const AI_PRODUCTS_INPUTS_MAX_JSON_CHARS = 7_500\n\n/**\n * A job's inputs for a target, as the console sends them: every value a\n * scalar, lists as JSON or as comma-separated ids, each bounded, and the\n * product's text shortened last until the whole fits what the create door\n * admits.\n */\nexport function aiProductsJobInputs(input: AiProductsJobInputs): Record<string, string> {\n switch (input.target) {\n case 'product': {\n const product = input.product\n const inputs: Record<string, string> = {\n target: 'product',\n productId: product.id ?? '',\n name: text(product.name, AI_PRODUCT_NAME_MAX_CHARS),\n type: product.type,\n text: product.text.slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: JSON.stringify(product.tags.slice(0, 10).map((tag) => tag.slice(0, 40))),\n categoryIds: product.categoryIds.filter((id) => ID.test(id)).slice(0, 10).join(','),\n options: JSON.stringify(\n product.options.slice(0, AI_PRODUCT_OPTIONS_MAX).map((option) => ({\n name: option.name.slice(0, AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: option.values.slice(0, AI_PRODUCT_OPTION_VALUES_MAX).map((value) => value.slice(0, 40)),\n })),\n ),\n imageUrl: (product.imageUrl ?? '').slice(0, 300),\n seoTitle: product.seoTitle.slice(0, 100),\n seoDescription: product.seoDescription.slice(0, 200),\n }\n while (JSON.stringify(inputs).length > AI_PRODUCTS_INPUTS_MAX_JSON_CHARS && inputs['text']) {\n inputs['text'] = inputs['text'].slice(0, Math.max(0, inputs['text'].length - 250))\n }\n return inputs\n }\n case 'bulk':\n return aiProductsBulkInputs(input.productIds)\n default:\n return { target: input.target }\n }\n}\n\n/** Why a `products` job's inputs cannot run, in words a person can act on. */\nexport const AI_PRODUCTS_NO_TARGET_COPY = 'This AI job does not say what to write for your products.'\nexport const AI_PRODUCTS_NO_NAME_COPY = 'Give the product a name before writing its copy.'\nexport const AI_PRODUCTS_NO_SELECTION_COPY = 'Pick the products to write copy for.'\nexport const AI_PRODUCTS_TOO_MANY_COPY = `Pick at most ${AI_PRODUCTS_BULK_MAX} products at a time.`\n\n/** A job's inputs read back, or the sentence that says why they cannot run. */\nexport function parseAiProductsJobInputs(raw: Readonly<Record<string, unknown>> | undefined): AiProductsJobInputs | string {\n const inputs = raw ?? {}\n const target = inputs['target']\n if (!(AI_PRODUCTS_TARGETS as readonly unknown[]).includes(target)) return AI_PRODUCTS_NO_TARGET_COPY\n if (target === 'product') {\n const name = text(inputs['name'], AI_PRODUCT_NAME_MAX_CHARS)\n if (!name) return AI_PRODUCTS_NO_NAME_COPY\n const type = inputs['type']\n const id = typeof inputs['productId'] === 'string' && ID.test(inputs['productId']) ? inputs['productId'] : null\n return {\n target,\n product: {\n id,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: typeof inputs['text'] === 'string' ? inputs['text'].trim().slice(0, AI_PRODUCT_TEXT_MAX_CHARS) : '',\n tags: jsonStrings(inputs['tags'], 10, 40),\n categoryIds: String(inputs['categoryIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n .slice(0, 10),\n options: jsonOptions(inputs['options']),\n imageUrl: text(inputs['imageUrl'], 300) || null,\n seoTitle: text(inputs['seoTitle'], 100),\n seoDescription: text(inputs['seoDescription'], 200),\n },\n }\n }\n if (target === 'bulk') {\n const ids = String(inputs['productIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n const unique = [...new Set(ids)]\n if (!unique.length) return AI_PRODUCTS_NO_SELECTION_COPY\n if (unique.length > AI_PRODUCTS_BULK_MAX) return AI_PRODUCTS_TOO_MANY_COPY\n return { target, productIds: unique }\n }\n return { target: target as 'catalog' | 'categories' }\n}\n\n/* ------------------------------------------------------------------------ *\n * Proposals\n * ------------------------------------------------------------------------ */\n\n/** A product's copy, as proposed. */\nexport interface AiProductCopy {\n description: string\n seoTitle: string\n seoDescription: string\n tags: string[]\n categoryIds: string[]\n /** One name per option the product had, in order. */\n optionNames: string[]\n}\n\n/** What became of the product's photo. */\nexport type AiProductPhotoRead =\n /** The photo was read. */\n | 'read'\n /** The product has no photo. */\n | 'none'\n /** The photo is not an asset of this site's media library, or its org's. */\n | 'not-in-library'\n /** The photo could not be read. */\n | 'unreadable'\n /** The model this job ran on does not read pictures. */\n | 'model'\n\nexport interface AiProductCopyProposal {\n kind: 'copy'\n product: { id: string | null; name: string }\n /** `null` when there was nothing to propose: see `skipped`. */\n values: AiProductCopy | null\n /** The categories `values` names, with the names they had when the job read them. */\n categories: Array<{ id: string; name: string }>\n /** The option names the product had, beside the ones proposed. */\n optionNamesBefore: string[]\n /** The facts the copy marks as missing, in square brackets, for a person to fill in. */\n gaps: string[]\n photo: AiProductPhotoRead\n /** Why a bulk pass proposed nothing for this product, in customer-safe words. */\n skipped: string | null\n notes: string[]\n}\n\nexport interface AiCatalogProduct {\n name: string\n type: AiProductType\n description: string\n tags: string[]\n options: AiProductOption[]\n seoTitle: string\n seoDescription: string\n /** What the product's photo should show; nothing is uploaded or linked. */\n photo: string\n /**\n * The suggested price for every variant, in US dollars, as `aiCatalogPrice`\n * kept it; `null` (or absent, on a proposal made before 2026-10-12) when\n * the model gave none in range, and the store's default price stands in.\n */\n price?: number | null\n}\n\nexport interface AiCatalogProposal {\n kind: 'catalog'\n products: AiCatalogProduct[]\n notes: string[]\n}\n\nexport type AiDiscountKind = 'percent' | 'fixed' | 'free_shipping'\n\nexport interface AiProposedDiscount {\n name: string\n /** The code a shopper types, upper case; `null` for an automatic discount. */\n code: string | null\n kind: AiDiscountKind\n /** A `percent` discount's percentage. */\n valuePct: number | null\n /** A `fixed` discount's amount off, in cents. */\n valueCents: number | null\n /** The smallest order it applies to, in cents; `null` for any order. */\n minSubtotalCents: number | null\n why: string\n}\n\nexport interface AiCategoriesProposal {\n kind: 'categories'\n categories: Array<{ name: string; why: string }>\n discounts: AiProposedDiscount[]\n notes: string[]\n}\n\nexport type AiProductsProposal = AiProductCopyProposal | AiCatalogProposal | AiCategoriesProposal\n\n/** Output ids, so a pass that runs again finds what it already proposed. */\nexport const AI_PRODUCTS_OUTPUT_IDS = {\n copy: (productId: string | null) => `copy:${productId ?? 'new'}`,\n catalog: 'catalog',\n categories: 'categories',\n} as const\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** An output's proposal, or `null` for an output that is not a products proposal. */\nexport function aiProductsProposalOf(output: Pick<AiJobOutput, 'resource' | 'proposal'> | null | undefined): AiProductsProposal | null {\n if (!output || output.resource !== 'product' || !isRecord(output.proposal)) return null\n const kind = output.proposal['kind']\n return kind === 'copy' || kind === 'catalog' || kind === 'categories'\n ? (output.proposal as unknown as AiProductsProposal)\n : null\n}\n\n/** A job's copy proposals, in the order the job made them. */\nexport function aiProductCopyProposals(job: Pick<AiJobSummary, 'outputs'> | null | undefined): AiProductCopyProposal[] {\n return (job?.outputs ?? [])\n .map((output) => aiProductsProposalOf(output))\n .filter((proposal): proposal is AiProductCopyProposal => proposal?.kind === 'copy')\n}\n\n/** A job's proposal of one kind, or `null`. */\nexport function aiProductsProposalOfKind<K extends AiProductsProposal['kind']>(\n job: Pick<AiJobSummary, 'outputs'> | null | undefined,\n kind: K,\n): Extract<AiProductsProposal, { kind: K }> | null {\n for (const output of job?.outputs ?? []) {\n const proposal = aiProductsProposalOf(output)\n if (proposal?.kind === kind) return proposal as Extract<AiProductsProposal, { kind: K }>\n }\n return null\n}\n\n/** The gaps a text marks for a person in square brackets, as `[material]`, each once. */\nexport function aiProductGaps(...texts: readonly string[]): string[] {\n const found = new Set<string>()\n for (const value of texts) {\n for (const match of value.matchAll(/\\[([^\\]\\n]{1,60})\\]/g)) found.add(match[1].trim())\n }\n return [...found].filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * The console's cards\n * ------------------------------------------------------------------------ */\n\n/** The option the import zone sets to write the imported products' copy. */\nexport const AI_PRODUCTS_IMPORT_WRITE_COPY = 'ai.writeCopy'\n\nconst BULK_BRIEF = /^Write storefront copy for (\\d+) products?$/\n\n/** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */\nexport function aiProductsBulkCount(job: Pick<AiJobSummary, 'brief'> | null | undefined): number | null {\n const match = BULK_BRIEF.exec(job?.brief ?? '')\n return match ? Number(match[1]) : null\n}\n"],"names":["AI_PRODUCTS_BULK_MAX","aiProductsBulkBrief","aiProductsBulkInputs","AI_PRODUCTS_TARGETS","AI_PRODUCT_TYPES","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_CATALOG_PRODUCTS","min","max","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_TAGS_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRICE_USD","aiCatalogPrice","raw","value","match","exec","Number","isFinite","cents","Math","round","AI_CATEGORIES_MAX","AI_DISCOUNTS_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_PROPOSAL_WHY_MAX_CHARS","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_CODE","aiProductMerchantWords","product","name","text","tags","join","ID","replace","trim","slice","jsonStrings","maxChars","parsed","JSON","parse","Array","isArray","map","entry","filter","Boolean","jsonOptions","record","values","AI_PRODUCTS_INPUTS_MAX_JSON_CHARS","aiProductsJobInputs","input","target","inputs","productId","id","type","stringify","tag","categoryIds","test","options","option","imageUrl","seoTitle","seoDescription","length","productIds","AI_PRODUCTS_NO_TARGET_COPY","AI_PRODUCTS_NO_NAME_COPY","AI_PRODUCTS_NO_SELECTION_COPY","AI_PRODUCTS_TOO_MANY_COPY","parseAiProductsJobInputs","includes","String","split","part","ids","unique","Set","AI_PRODUCTS_OUTPUT_IDS","copy","catalog","categories","isRecord","aiProductsProposalOf","output","resource","proposal","kind","aiProductCopyProposals","job","outputs","aiProductsProposalOfKind","aiProductGaps","texts","found","matchAll","add","AI_PRODUCTS_IMPORT_WRITE_COPY","BULK_BRIEF","aiProductsBulkCount","brief"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,oBAAoB,EAAEC,mBAAmB,EAAEC,oBAAoB,QAAQ,wBAAoB;AAEpG,SAASF,oBAAoB,EAAEC,mBAAmB,GAAE;AAEpD;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,OAAO,MAAME,sBAAsB;IAAC;IAAW;IAAQ;IAAW;CAAa,CAAS;AAGxF,OAAO,MAAMC,mBAAmB;IAAC;IAAY;IAAW;CAAU,CAAS;AAG3E,+CAA+C,GAC/C,OAAO,MAAMC,4BAA4B,IAAG;AAE5C,oEAAoE,GACpE,OAAO,MAAMC,4BAA4B,KAAK;AAE9C,8CAA8C,GAC9C,OAAO,MAAMC,mCAAmC,KAAK;AAErD,OAAO,MAAMC,sBAAsB,EAAC;AACpC,OAAO,MAAMC,2BAA2B,GAAE;AAE1C,sDAAsD,GACtD,OAAO,MAAMC,4BAA4B,EAAC;AAE1C,oEAAoE,GACpE,OAAO,MAAMC,+BAA+B,IAAG;AAE/C,iEAAiE,GACjE,OAAO,MAAMC,yBAAyB,EAAC;AACvC,OAAO,MAAMC,+BAA+B,GAAE;AAC9C,OAAO,MAAMC,mCAAmC,GAAE;AAElD,gDAAgD,GAChD,OAAO,MAAMC,sBAAsB;IAAEC,KAAK;IAAGC,KAAK;AAAG,EAAU;AAE/D;;;;CAIC,GACD,OAAO,MAAMC,mCAAmC,IAAG;AACnD,OAAO,MAAMC,sBAAsB,EAAC;AAEpC,oGAAoG,GACpG,OAAO,MAAMC,yBAAyB,EAAC;AACvC,OAAO,MAAMC,+BAA+B,EAAC;AAC7C,OAAO,MAAMC,oCAAoC,GAAE;AAEnD,kEAAkE,GAClE,OAAO,MAAMC,6BAA6B,IAAG;AAE7C;;;;;;;;;CASC,GACD,OAAO,MAAMC,uBAAuB;IAAER,KAAK;IAAGC,KAAK;AAAM,EAAU;AAEnE;;;;;;;CAOC,GACD,OAAO,SAASQ,eAAeC,GAAY;IACzC,IAAIC,QAAuB;IAC3B,IAAI,OAAOD,QAAQ,UAAUC,QAAQD;SAChC,IAAI,OAAOA,QAAQ,UAAU;QAChC,MAAME,QAAQ,8DAA8DC,IAAI,CAACH;QACjFC,QAAQC,QAAQE,OAAOF,KAAK,CAAC,EAAE,IAAI;IACrC;IACA,IAAID,UAAU,QAAQ,CAACG,OAAOC,QAAQ,CAACJ,QAAQ,OAAO;IACtD,MAAMK,QAAQC,KAAKC,KAAK,CAACP,QAAQ;IACjC,IAAIK,QAAQR,qBAAqBR,GAAG,GAAG,OAAOgB,QAAQR,qBAAqBP,GAAG,GAAG,KAAK,OAAO;IAC7F,OAAOe,QAAQ;AACjB;AAEA,OAAO,MAAMG,oBAAoB,GAAE;AACnC,OAAO,MAAMC,mBAAmB,EAAC;AACjC,OAAO,MAAMC,6BAA6B,GAAE;AAC5C,OAAO,MAAMC,4BAA4B,IAAG;AAE5C,sGAAsG,GACtG,OAAO,MAAMC,0BAA0B,GAAE;AACzC,OAAO,MAAMC,4BAA4B,KAAK;AAE9C,iFAAiF,GACjF,OAAO,MAAMC,mBAAmB,qBAAoB;AA+BpD,oFAAoF,GACpF,OAAO,SAASC,uBAAuBC,OAAuD;IAC5F,OAAO;QAACA,QAAQC,IAAI;QAAED,QAAQE,IAAI;WAAKF,QAAQG,IAAI;KAAC,CAACC,IAAI,CAAC;AAC5D;AAQA,MAAMC,KAAK;AAEX,MAAMH,OAAO,CAAClB,OAAgBV,MAC5B,OAAOU,UAAU,WAAWA,MAAMsB,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGC,KAAK,CAAC,GAAGlC,OAAO;AAEhF,6EAA6E,GAC7E,SAASmC,YAAYzB,KAAc,EAAEV,GAAW,EAAEoC,QAAgB;IAChE,IAAI,OAAO1B,UAAU,YAAY,CAACA,MAAMuB,IAAI,IAAI,OAAO,EAAE;IACzD,IAAI;QACF,MAAMI,SAAkBC,KAAKC,KAAK,CAAC7B;QACnC,IAAI,CAAC8B,MAAMC,OAAO,CAACJ,SAAS,OAAO,EAAE;QACrC,OAAOA,OACJK,GAAG,CAAC,CAACC,QAAUf,KAAKe,OAAOP,WAC3BQ,MAAM,CAACC,SACPX,KAAK,CAAC,GAAGlC;IACd,EAAE,eAAM;QACN,OAAO,EAAE;IACX;AACF;AAEA,SAAS8C,YAAYpC,KAAc;IACjC,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMuB,IAAI,IAAI,OAAO,EAAE;IACzD,IAAI;QACF,MAAMI,SAAkBC,KAAKC,KAAK,CAAC7B;QACnC,IAAI,CAAC8B,MAAMC,OAAO,CAACJ,SAAS,OAAO,EAAE;QACrC,OAAOA,OACJK,GAAG,CAAC,CAACC;YACJ,MAAMI,SAAUJ,gBAAAA,QAAS,CAAC;YAC1B,OAAO;gBACLhB,MAAMC,KAAKmB,MAAM,CAAC,OAAO,EAAElD;gBAC3BmD,QAAQR,MAAMC,OAAO,CAACM,MAAM,CAAC,SAAS,IAClCA,MAAM,CAAC,SAAS,CAACL,GAAG,CAAC,CAAChC,QAAUkB,KAAKlB,OAAO,KAAKkC,MAAM,CAACC,SAASX,KAAK,CAAC,GAAGtC,gCAC1E,EAAE;YACR;QACF,GACCsC,KAAK,CAAC,GAAGvC;IACd,EAAE,eAAM;QACN,OAAO,EAAE;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,MAAMsD,oCAAoC,KAAK;AAEtD;;;;;CAKC,GACD,OAAO,SAASC,oBAAoBC,KAA0B;IAC5D,OAAQA,MAAMC,MAAM;QAClB,KAAK;YAAW;oBAID1B,aAYAA;gBAfb,MAAMA,UAAUyB,MAAMzB,OAAO;gBAC7B,MAAM2B,SAAiC;oBACrCD,QAAQ;oBACRE,SAAS,GAAE5B,cAAAA,QAAQ6B,EAAE,YAAV7B,cAAc;oBACzBC,MAAMC,KAAKF,QAAQC,IAAI,EAAEvC;oBACzBoE,MAAM9B,QAAQ8B,IAAI;oBAClB5B,MAAMF,QAAQE,IAAI,CAACM,KAAK,CAAC,GAAG7C;oBAC5BwC,MAAMS,KAAKmB,SAAS,CAAC/B,QAAQG,IAAI,CAACK,KAAK,CAAC,GAAG,IAAIQ,GAAG,CAAC,CAACgB,MAAQA,IAAIxB,KAAK,CAAC,GAAG;oBACzEyB,aAAajC,QAAQiC,WAAW,CAACf,MAAM,CAAC,CAACW,KAAOxB,GAAG6B,IAAI,CAACL,KAAKrB,KAAK,CAAC,GAAG,IAAIJ,IAAI,CAAC;oBAC/E+B,SAASvB,KAAKmB,SAAS,CACrB/B,QAAQmC,OAAO,CAAC3B,KAAK,CAAC,GAAGvC,wBAAwB+C,GAAG,CAAC,CAACoB,SAAY,CAAA;4BAChEnC,MAAMmC,OAAOnC,IAAI,CAACO,KAAK,CAAC,GAAGrC;4BAC3BmD,QAAQc,OAAOd,MAAM,CAACd,KAAK,CAAC,GAAGtC,8BAA8B8C,GAAG,CAAC,CAAChC,QAAUA,MAAMwB,KAAK,CAAC,GAAG;wBAC7F,CAAA;oBAEF6B,UAAU,EAACrC,oBAAAA,QAAQqC,QAAQ,YAAhBrC,oBAAoB,IAAIQ,KAAK,CAAC,GAAG;oBAC5C8B,UAAUtC,QAAQsC,QAAQ,CAAC9B,KAAK,CAAC,GAAG;oBACpC+B,gBAAgBvC,QAAQuC,cAAc,CAAC/B,KAAK,CAAC,GAAG;gBAClD;gBACA,MAAOI,KAAKmB,SAAS,CAACJ,QAAQa,MAAM,GAAGjB,qCAAqCI,MAAM,CAAC,OAAO,CAAE;oBAC1FA,MAAM,CAAC,OAAO,GAAGA,MAAM,CAAC,OAAO,CAACnB,KAAK,CAAC,GAAGlB,KAAKhB,GAAG,CAAC,GAAGqD,MAAM,CAAC,OAAO,CAACa,MAAM,GAAG;gBAC/E;gBACA,OAAOb;YACT;QACA,KAAK;YACH,OAAOpE,qBAAqBkE,MAAMgB,UAAU;QAC9C;YACE,OAAO;gBAAEf,QAAQD,MAAMC,MAAM;YAAC;IAClC;AACF;AAEA,4EAA4E,GAC5E,OAAO,MAAMgB,6BAA6B,4DAA2D;AACrG,OAAO,MAAMC,2BAA2B,mDAAkD;AAC1F,OAAO,MAAMC,gCAAgC,uCAAsC;AACnF,OAAO,MAAMC,4BAA4B,CAAC,aAAa,EAAExF,qBAAqB,oBAAoB,CAAC,CAAA;AAEnG,6EAA6E,GAC7E,OAAO,SAASyF,yBAAyB/D,GAAkD;IACzF,MAAM4C,SAAS5C,cAAAA,MAAO,CAAC;IACvB,MAAM2C,SAASC,MAAM,CAAC,SAAS;IAC/B,IAAI,CAAC,AAACnE,oBAA2CuF,QAAQ,CAACrB,SAAS,OAAOgB;IAC1E,IAAIhB,WAAW,WAAW;YAaAC;QAZxB,MAAM1B,OAAOC,KAAKyB,MAAM,CAAC,OAAO,EAAEjE;QAClC,IAAI,CAACuC,MAAM,OAAO0C;QAClB,MAAMb,OAAOH,MAAM,CAAC,OAAO;QAC3B,MAAME,KAAK,OAAOF,MAAM,CAAC,YAAY,KAAK,YAAYtB,GAAG6B,IAAI,CAACP,MAAM,CAAC,YAAY,IAAIA,MAAM,CAAC,YAAY,GAAG;QAC3G,OAAO;YACLD;YACA1B,SAAS;gBACP6B;gBACA5B;gBACA6B,MAAM,AAACrE,iBAAwCsF,QAAQ,CAACjB,QAASA,OAAyB;gBAC1F5B,MAAM,OAAOyB,MAAM,CAAC,OAAO,KAAK,WAAWA,MAAM,CAAC,OAAO,CAACpB,IAAI,GAAGC,KAAK,CAAC,GAAG7C,6BAA6B;gBACvGwC,MAAMM,YAAYkB,MAAM,CAAC,OAAO,EAAE,IAAI;gBACtCM,aAAae,QAAOrB,sBAAAA,MAAM,CAAC,cAAc,YAArBA,sBAAyB,IAC1CsB,KAAK,CAAC,KACNjC,GAAG,CAAC,CAACkC,OAASA,KAAK3C,IAAI,IACvBW,MAAM,CAAC,CAACgC,OAAS7C,GAAG6B,IAAI,CAACgB,OACzB1C,KAAK,CAAC,GAAG;gBACZ2B,SAASf,YAAYO,MAAM,CAAC,UAAU;gBACtCU,UAAUnC,KAAKyB,MAAM,CAAC,WAAW,EAAE,QAAQ;gBAC3CW,UAAUpC,KAAKyB,MAAM,CAAC,WAAW,EAAE;gBACnCY,gBAAgBrC,KAAKyB,MAAM,CAAC,iBAAiB,EAAE;YACjD;QACF;IACF;IACA,IAAID,WAAW,QAAQ;YACFC;QAAnB,MAAMwB,MAAMH,QAAOrB,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB,IACxCsB,KAAK,CAAC,KACNjC,GAAG,CAAC,CAACkC,OAASA,KAAK3C,IAAI,IACvBW,MAAM,CAAC,CAACgC,OAAS7C,GAAG6B,IAAI,CAACgB;QAC5B,MAAME,SAAS;eAAI,IAAIC,IAAIF;SAAK;QAChC,IAAI,CAACC,OAAOZ,MAAM,EAAE,OAAOI;QAC3B,IAAIQ,OAAOZ,MAAM,GAAGnF,sBAAsB,OAAOwF;QACjD,OAAO;YAAEnB;YAAQe,YAAYW;QAAO;IACtC;IACA,OAAO;QAAE1B,QAAQA;IAAmC;AACtD;AAgGA,0EAA0E,GAC1E,OAAO,MAAM4B,yBAAyB;IACpCC,MAAM,CAAC3B,YAA6B,CAAC,KAAK,EAAEA,oBAAAA,YAAa,OAAO;IAChE4B,SAAS;IACTC,YAAY;AACd,EAAU;AAEV,MAAMC,WAAW,CAAC1E,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAAC8B,MAAMC,OAAO,CAAC/B;AAEhE,mFAAmF,GACnF,OAAO,SAAS2E,qBAAqBC,MAAqE;IACxG,IAAI,CAACA,UAAUA,OAAOC,QAAQ,KAAK,aAAa,CAACH,SAASE,OAAOE,QAAQ,GAAG,OAAO;IACnF,MAAMC,OAAOH,OAAOE,QAAQ,CAAC,OAAO;IACpC,OAAOC,SAAS,UAAUA,SAAS,aAAaA,SAAS,eACpDH,OAAOE,QAAQ,GAChB;AACN;AAEA,4DAA4D,GAC5D,OAAO,SAASE,uBAAuBC,GAAqD;;IAC1F,OAAO,SAACA,uBAAAA,IAAKC,OAAO,mBAAI,EAAE,EACvBlD,GAAG,CAAC,CAAC4C,SAAWD,qBAAqBC,SACrC1C,MAAM,CAAC,CAAC4C,WAAgDA,CAAAA,4BAAAA,SAAUC,IAAI,MAAK;AAChF;AAEA,6CAA6C,GAC7C,OAAO,SAASI,yBACdF,GAAqD,EACrDF,IAAO;;IAEP,KAAK,MAAMH,kBAAUK,uBAAAA,IAAKC,OAAO,mBAAI,EAAE,CAAE;QACvC,MAAMJ,WAAWH,qBAAqBC;QACtC,IAAIE,CAAAA,4BAAAA,SAAUC,IAAI,MAAKA,MAAM,OAAOD;IACtC;IACA,OAAO;AACT;AAEA,uFAAuF,GACvF,OAAO,SAASM,cAAc,GAAGC,KAAwB;IACvD,MAAMC,QAAQ,IAAIjB;IAClB,KAAK,MAAMrE,SAASqF,MAAO;QACzB,KAAK,MAAMpF,SAASD,MAAMuF,QAAQ,CAAC,wBAAyBD,MAAME,GAAG,CAACvF,KAAK,CAAC,EAAE,CAACsB,IAAI;IACrF;IACA,OAAO;WAAI+D;KAAM,CAACpD,MAAM,CAACC;AAC3B;AAEA;;4EAE4E,GAE5E,0EAA0E,GAC1E,OAAO,MAAMsD,gCAAgC,eAAc;AAE3D,MAAMC,aAAa;AAEnB,oGAAoG,GACpG,OAAO,SAASC,oBAAoBV,GAAmD;;IACrF,MAAMhF,QAAQyF,WAAWxF,IAAI,SAAC+E,uBAAAA,IAAKW,KAAK,mBAAI;IAC5C,OAAO3F,QAAQE,OAAOF,KAAK,CAAC,EAAE,IAAI;AACpC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-products.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AiJobOutput, AiJobSummary } from './ai-jobs.types'\nimport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief, aiProductsBulkInputs } from './ai-products-bulk'\n\nexport { AI_PRODUCTS_BULK_MAX, aiProductsBulkBrief }\n\n/**\n * COMMERCE BY AI (AGL-2916): what a `products` job is asked, and what it\n * proposes. Pure, so the step that asks, the console cards that start a job\n * and show its proposals, and the eval harness all read one shape.\n *\n * `inputs.target` names the work:\n *\n * - `product` — one product's copy, from what its editor holds right now:\n * the name, the text, the tags, the categories and options it has, and the\n * first photo. The editor may hold a product nobody has saved yet, so the\n * job never reads a product document for this target.\n * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one\n * product a pass, each read from the catalog as it is reached.\n * - `catalog` — a store's first products from the brief: names, copy, tags\n * and options, what each photo should show, and a suggested price by what\n * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.\n * - `categories` — store categories and a first set of discounts from the brief.\n *\n * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the\n * job writes nothing. The commerce plugin's own surfaces write what a person\n * accepts, through the doors those surfaces already use.\n */\n\nexport const AI_PRODUCTS_TARGETS = ['product', 'bulk', 'catalog', 'categories'] as const\nexport type AiProductsTarget = (typeof AI_PRODUCTS_TARGETS)[number]\n\nexport const AI_PRODUCT_TYPES = ['physical', 'digital', 'service'] as const\nexport type AiProductType = (typeof AI_PRODUCT_TYPES)[number]\n\n/** The longest product name the editor saves. */\nexport const AI_PRODUCT_NAME_MAX_CHARS = 120\n\n/** How much of a product's existing description a request carries. */\nexport const AI_PRODUCT_TEXT_MAX_CHARS = 2_000\n\n/** The longest description the job proposes. */\nexport const AI_PRODUCT_DESCRIPTION_MAX_CHARS = 1_200\n\nexport const AI_PRODUCT_TAGS_MAX = 8\nexport const AI_PRODUCT_TAG_MAX_CHARS = 30\n\n/** The most categories one product's proposal names. */\nexport const AI_PRODUCT_CATEGORIES_MAX = 3\n\n/** The most of a site's categories a request lists to choose from. */\nexport const AI_PRODUCT_CATEGORY_LIST_MAX = 100\n\n/** A product's option axes, as the commerce editor bounds them. */\nexport const AI_PRODUCT_OPTIONS_MAX = 3\nexport const AI_PRODUCT_OPTION_VALUES_MAX = 25\nexport const AI_PRODUCT_OPTION_NAME_MAX_CHARS = 30\n\n/** How many products a catalog proposal holds. */\nexport const AI_CATALOG_PRODUCTS = { min: 1, max: 12 } as const\n\n/**\n * A proposed product is shorter than a written one: its description and tags\n * are a start the owner completes, and twelve of them at every bound still fit\n * one answer (`ai-job-products-step.spec.ts` measures it).\n */\nexport const AI_CATALOG_DESCRIPTION_MAX_CHARS = 600\nexport const AI_CATALOG_TAGS_MAX = 6\n\n/** A proposed product's options: few axes and few short values, so its variants stay a short list. */\nexport const AI_CATALOG_OPTIONS_MAX = 2\nexport const AI_CATALOG_OPTION_VALUES_MAX = 6\nexport const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30\n\n/** What a proposed product's photo should show, in one sentence. */\nexport const AI_CATALOG_PHOTO_MAX_CHARS = 160\n\n/**\n * A proposed product's suggested price, in US dollars — the one currency a\n * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,\n * 2026-10-12): a store's first products priced by what each one is (a bar\n * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead\n * of every one at the commerce default. Never $0 or less, nothing past what\n * a small shop's first product plausibly lists at; a price outside the range,\n * or none, is no price, and the writer gives the product its default price.\n * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.\n */\nexport const AI_CATALOG_PRICE_USD = { min: 1, max: 5_000 } as const\n\n/**\n * A proposed price as code keeps it: a finite number of dollars (or a plain\n * \"$12.50\" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to\n * whole cents; anything else — missing, zero, negative, absurd, not a number\n * — is `null`, so the product takes the store's default price for the owner\n * to change. The model is never re-asked for a price: a bad one costs the\n * product only its suggestion.\n */\nexport function aiCatalogPrice(raw: unknown): number | null {\n let value: number | null = null\n if (typeof raw === 'number') value = raw\n else if (typeof raw === 'string') {\n const match = /^\\s*(?:US\\s*)?\\$?\\s*(\\d{1,6}(?:\\.\\d{1,2})?)\\s*(?:USD)?\\s*$/i.exec(raw)\n value = match ? Number(match[1]) : null\n }\n if (value === null || !Number.isFinite(value)) return null\n const cents = Math.round(value * 100)\n if (cents < AI_CATALOG_PRICE_USD.min * 100 || cents > AI_CATALOG_PRICE_USD.max * 100) return null\n return cents / 100\n}\n\nexport const AI_CATEGORIES_MAX = 12\nexport const AI_DISCOUNTS_MAX = 5\nexport const AI_CATEGORY_NAME_MAX_CHARS = 60\nexport const AI_PROPOSAL_WHY_MAX_CHARS = 200\n\n/** The highest percentage a proposed discount takes off, and the most a fixed one takes, in dollars. */\nexport const AI_DISCOUNT_MAX_PERCENT = 90\nexport const AI_DISCOUNT_MAX_FIXED_USD = 1_000\n\n/** A discount code: letters, digits, dashes and underscores, stored upper case. */\nexport const AI_DISCOUNT_CODE = /^[A-Z0-9_-]{3,20}$/\n\n/** An option axis. */\nexport interface AiProductOption {\n name: string\n values: string[]\n}\n\n/** One product as its editor hands it to a `product` job. */\nexport interface AiProductFacts {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: AiProductType\n /** The product's current description, cut to `AI_PRODUCT_TEXT_MAX_CHARS`. */\n text: string\n tags: string[]\n categoryIds: string[]\n options: AiProductOption[]\n /** The first photo's stored media value, or `null`. */\n imageUrl: string | null\n seoTitle: string\n seoDescription: string\n}\n\n/** A site's product category, as a request lists it to choose from. */\nexport interface AiProductCategory {\n id: string\n name: string\n}\n\n/** The merchant's own words about a product, which a claim in its copy may repeat. */\nexport function aiProductMerchantWords(product: Pick<AiProductFacts, 'name' | 'text' | 'tags'>): string {\n return [product.name, product.text, ...product.tags].join('\\n')\n}\n\nexport type AiProductsJobInputs =\n | { target: 'product'; product: AiProductFacts }\n | { target: 'bulk'; productIds: string[] }\n | { target: 'catalog' }\n | { target: 'categories' }\n\nconst ID = /^[A-Za-z0-9_-]{1,64}$/\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\n/** A list stored as JSON, read defensively: strings only, trimmed, bounded. */\nfunction jsonStrings(value: unknown, max: number, maxChars: number): string[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => text(entry, maxChars))\n .filter(Boolean)\n .slice(0, max)\n } catch {\n return []\n }\n}\n\nfunction jsonOptions(value: unknown): AiProductOption[] {\n if (typeof value !== 'string' || !value.trim()) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .map((entry) => {\n const record = (entry ?? {}) as Record<string, unknown>\n return {\n name: text(record['name'], AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: Array.isArray(record['values'])\n ? record['values'].map((value) => text(value, 40)).filter(Boolean).slice(0, AI_PRODUCT_OPTION_VALUES_MAX)\n : [],\n }\n })\n .slice(0, AI_PRODUCT_OPTIONS_MAX)\n } catch {\n return []\n }\n}\n\n/**\n * How large a job's inputs may be once written as JSON: under the 8,000\n * characters the create door admits, with room for the door's own keys.\n */\nexport const AI_PRODUCTS_INPUTS_MAX_JSON_CHARS = 7_500\n\n/**\n * A job's inputs for a target, as the console sends them: every value a\n * scalar, lists as JSON or as comma-separated ids, each bounded, and the\n * product's text shortened last until the whole fits what the create door\n * admits.\n */\nexport function aiProductsJobInputs(input: AiProductsJobInputs): Record<string, string> {\n switch (input.target) {\n case 'product': {\n const product = input.product\n const inputs: Record<string, string> = {\n target: 'product',\n productId: product.id ?? '',\n name: text(product.name, AI_PRODUCT_NAME_MAX_CHARS),\n type: product.type,\n text: product.text.slice(0, AI_PRODUCT_TEXT_MAX_CHARS),\n tags: JSON.stringify(product.tags.slice(0, 10).map((tag) => tag.slice(0, 40))),\n categoryIds: product.categoryIds.filter((id) => ID.test(id)).slice(0, 10).join(','),\n options: JSON.stringify(\n product.options.slice(0, AI_PRODUCT_OPTIONS_MAX).map((option) => ({\n name: option.name.slice(0, AI_PRODUCT_OPTION_NAME_MAX_CHARS),\n values: option.values.slice(0, AI_PRODUCT_OPTION_VALUES_MAX).map((value) => value.slice(0, 40)),\n })),\n ),\n imageUrl: (product.imageUrl ?? '').slice(0, 300),\n seoTitle: product.seoTitle.slice(0, 100),\n seoDescription: product.seoDescription.slice(0, 200),\n }\n while (JSON.stringify(inputs).length > AI_PRODUCTS_INPUTS_MAX_JSON_CHARS && inputs['text']) {\n inputs['text'] = inputs['text'].slice(0, Math.max(0, inputs['text'].length - 250))\n }\n return inputs\n }\n case 'bulk':\n return aiProductsBulkInputs(input.productIds)\n default:\n return { target: input.target }\n }\n}\n\n/** Why a `products` job's inputs cannot run, in words a person can act on. */\nexport const AI_PRODUCTS_NO_TARGET_COPY = 'This AI job does not say what to write for your products.'\nexport const AI_PRODUCTS_NO_NAME_COPY = 'Give the product a name before writing its copy.'\nexport const AI_PRODUCTS_NO_SELECTION_COPY = 'Pick the products to write copy for.'\nexport const AI_PRODUCTS_TOO_MANY_COPY = `Pick at most ${AI_PRODUCTS_BULK_MAX} products at a time.`\n\n/** A job's inputs read back, or the sentence that says why they cannot run. */\nexport function parseAiProductsJobInputs(raw: Readonly<Record<string, unknown>> | undefined): AiProductsJobInputs | string {\n const inputs = raw ?? {}\n const target = inputs['target']\n if (!(AI_PRODUCTS_TARGETS as readonly unknown[]).includes(target)) return AI_PRODUCTS_NO_TARGET_COPY\n if (target === 'product') {\n const name = text(inputs['name'], AI_PRODUCT_NAME_MAX_CHARS)\n if (!name) return AI_PRODUCTS_NO_NAME_COPY\n const type = inputs['type']\n const id = typeof inputs['productId'] === 'string' && ID.test(inputs['productId']) ? inputs['productId'] : null\n return {\n target,\n product: {\n id,\n name,\n type: (AI_PRODUCT_TYPES as readonly unknown[]).includes(type) ? (type as AiProductType) : 'physical',\n text: typeof inputs['text'] === 'string' ? inputs['text'].trim().slice(0, AI_PRODUCT_TEXT_MAX_CHARS) : '',\n tags: jsonStrings(inputs['tags'], 10, 40),\n categoryIds: String(inputs['categoryIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n .slice(0, 10),\n options: jsonOptions(inputs['options']),\n imageUrl: text(inputs['imageUrl'], 300) || null,\n seoTitle: text(inputs['seoTitle'], 100),\n seoDescription: text(inputs['seoDescription'], 200),\n },\n }\n }\n if (target === 'bulk') {\n const ids = String(inputs['productIds'] ?? '')\n .split(',')\n .map((part) => part.trim())\n .filter((part) => ID.test(part))\n const unique = [...new Set(ids)]\n if (!unique.length) return AI_PRODUCTS_NO_SELECTION_COPY\n if (unique.length > AI_PRODUCTS_BULK_MAX) return AI_PRODUCTS_TOO_MANY_COPY\n return { target, productIds: unique }\n }\n return { target: target as 'catalog' | 'categories' }\n}\n\n/* ------------------------------------------------------------------------ *\n * Proposals\n * ------------------------------------------------------------------------ */\n\n/** A product's copy, as proposed. */\nexport interface AiProductCopy {\n description: string\n seoTitle: string\n seoDescription: string\n tags: string[]\n categoryIds: string[]\n /** One name per option the product had, in order. */\n optionNames: string[]\n}\n\n/** What became of the product's photo. */\nexport type AiProductPhotoRead =\n /** The photo was read. */\n | 'read'\n /** The product has no photo. */\n | 'none'\n /** The photo is not an asset of this site's media library, or its org's. */\n | 'not-in-library'\n /** The photo could not be read. */\n | 'unreadable'\n /** The model this job ran on does not read pictures. */\n | 'model'\n\nexport interface AiProductCopyProposal {\n kind: 'copy'\n product: { id: string | null; name: string }\n /** `null` when there was nothing to propose: see `skipped`. */\n values: AiProductCopy | null\n /** The categories `values` names, with the names they had when the job read them. */\n categories: Array<{ id: string; name: string }>\n /** The option names the product had, beside the ones proposed. */\n optionNamesBefore: string[]\n /** The facts the copy marks as missing, in square brackets, for a person to fill in. */\n gaps: string[]\n photo: AiProductPhotoRead\n /** Why a bulk pass proposed nothing for this product, in customer-safe words. */\n skipped: string | null\n notes: string[]\n}\n\nexport interface AiCatalogProduct {\n name: string\n type: AiProductType\n description: string\n tags: string[]\n options: AiProductOption[]\n seoTitle: string\n seoDescription: string\n /** What the product's photo should show; nothing is uploaded or linked. */\n photo: string\n /**\n * The suggested price for every variant, in US dollars, as `aiCatalogPrice`\n * kept it; `null` (or absent, on a proposal made before 2026-10-12) when\n * the model gave none in range, and the store's default price stands in.\n */\n price?: number | null\n}\n\nexport interface AiCatalogProposal {\n kind: 'catalog'\n products: AiCatalogProduct[]\n notes: string[]\n}\n\n// ── A product a shopper would buy (AGL-4065) ────────────────────────────\n\n/**\n * The re-ask a catalog whose names are kinds of product is answered with\n * (AGL-4065). The 2026-10-25 Berm & Bolt Moto Parts start listed \"Riding Gear\n * Item\", \"OEM ATV Parts Order\" and \"Aftermarket Dirt Bike Part\" at suggested\n * prices, from a brief that named kinds of part and no product.\n */\nexport const AI_CATALOG_NAME_REASK =\n 'Name real products a shopper would buy, never a category with “Item” or “Order” after it; where the brief names kinds of product and no product, name each one a plain item of one of those kinds, never a brand or a model the brief does not name.'\n\n/** The code a catalog's kind-for-a-name is refused under. */\nexport const AI_CATALOG_NAME_CODE = 'catalog-name-kind'\n\n/** A name's last word that names a kind of thing, or a transaction, never one thing a shopper buys. */\nconst KIND_HEAD =\n /\\b(?:items?|orders?|products?|parts?|things?|options?|selections?|assortments?|stuff|goods|merchandise|gear|accessor(?:y|ies)|suppl(?:y|ies)|equipment|essentials?|requests?|inquir(?:y|ies))$/i\n\n/** Makes a product name carries only where the brief names them: vehicles, apparel and electronics most of all. */\nconst BRANDS =\n /\\b(?:porsche|ferrari|lamborghini|bmw|mercedes|audi|volkswagen|toyota|honda|yamaha|kawasaki|suzuki|ktm|ducati|harley(?:-davidson)?|husqvarna|gasgas|polaris|can-am|tesla|ford|chevrolet|chevy|dodge|jeep|nissan|subaru|mazda|nike|adidas|reebok|samsung|iphone|playstation|xbox|nintendo|starbucks|coca-cola|pepsi)\\b/i\n\n/** A model number: letters and figures run together, as \"CRF450R\" or \"KX250\". */\nconst MODEL_CODE = /\\b(?=[A-Z0-9-]*\\d)(?=[A-Z0-9-]*[A-Z])[A-Z0-9][A-Z0-9-]{3,}\\b/\n\n/** The make a text names that the brief does not (AGL-4065), or `null`: a sample row or a product names none. Pure. */\nexport function aiTextNamesBrand(text: string, brief: string): string | null {\n const brand = BRANDS.exec(text)?.[0]\n return brand && !brief.toLowerCase().includes(brand.toLowerCase()) ? brand : null\n}\n\n/**\n * Why a proposed product's name is not one a shopper would buy (AGL-4065), or\n * `null`: a kind of product with a word like \"Item\", \"Order\" or \"Part\" after\n * it, or a make or a model number the brief does not name. \"Dirt Bike Tire\"\n * and \"Motocross helmet\" are products; \"Riding Gear Item\" is a kind. Pure.\n */\nexport function aiCatalogNameProblem(name: string, brief: string): string | null {\n const plain = name.replace(/\\s+/g, ' ').trim()\n if (!plain) return null\n const words = brief.toLowerCase()\n if (KIND_HEAD.test(plain.replace(/[^\\p{L}\\p{N}]+$/u, ''))) return `“${plain}” names a kind of product, not one a shopper buys`\n if (aiTextNamesBrand(plain, brief)) return `“${plain}” names a make the brief does not`\n const model = MODEL_CODE.exec(plain)?.[0]\n if (model && !words.includes(model.toLowerCase())) return `“${plain}” names a model the brief does not`\n return null\n}\n\nexport type AiDiscountKind = 'percent' | 'fixed' | 'free_shipping'\n\nexport interface AiProposedDiscount {\n name: string\n /** The code a shopper types, upper case; `null` for an automatic discount. */\n code: string | null\n kind: AiDiscountKind\n /** A `percent` discount's percentage. */\n valuePct: number | null\n /** A `fixed` discount's amount off, in cents. */\n valueCents: number | null\n /** The smallest order it applies to, in cents; `null` for any order. */\n minSubtotalCents: number | null\n why: string\n}\n\nexport interface AiCategoriesProposal {\n kind: 'categories'\n categories: Array<{ name: string; why: string }>\n discounts: AiProposedDiscount[]\n notes: string[]\n}\n\nexport type AiProductsProposal = AiProductCopyProposal | AiCatalogProposal | AiCategoriesProposal\n\n/** Output ids, so a pass that runs again finds what it already proposed. */\nexport const AI_PRODUCTS_OUTPUT_IDS = {\n copy: (productId: string | null) => `copy:${productId ?? 'new'}`,\n catalog: 'catalog',\n categories: 'categories',\n} as const\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** An output's proposal, or `null` for an output that is not a products proposal. */\nexport function aiProductsProposalOf(output: Pick<AiJobOutput, 'resource' | 'proposal'> | null | undefined): AiProductsProposal | null {\n if (!output || output.resource !== 'product' || !isRecord(output.proposal)) return null\n const kind = output.proposal['kind']\n return kind === 'copy' || kind === 'catalog' || kind === 'categories'\n ? (output.proposal as unknown as AiProductsProposal)\n : null\n}\n\n/** A job's copy proposals, in the order the job made them. */\nexport function aiProductCopyProposals(job: Pick<AiJobSummary, 'outputs'> | null | undefined): AiProductCopyProposal[] {\n return (job?.outputs ?? [])\n .map((output) => aiProductsProposalOf(output))\n .filter((proposal): proposal is AiProductCopyProposal => proposal?.kind === 'copy')\n}\n\n/** A job's proposal of one kind, or `null`. */\nexport function aiProductsProposalOfKind<K extends AiProductsProposal['kind']>(\n job: Pick<AiJobSummary, 'outputs'> | null | undefined,\n kind: K,\n): Extract<AiProductsProposal, { kind: K }> | null {\n for (const output of job?.outputs ?? []) {\n const proposal = aiProductsProposalOf(output)\n if (proposal?.kind === kind) return proposal as Extract<AiProductsProposal, { kind: K }>\n }\n return null\n}\n\n/** The gaps a text marks for a person in square brackets, as `[material]`, each once. */\nexport function aiProductGaps(...texts: readonly string[]): string[] {\n const found = new Set<string>()\n for (const value of texts) {\n for (const match of value.matchAll(/\\[([^\\]\\n]{1,60})\\]/g)) found.add(match[1].trim())\n }\n return [...found].filter(Boolean)\n}\n\n/* ------------------------------------------------------------------------ *\n * The console's cards\n * ------------------------------------------------------------------------ */\n\n/** The option the import zone sets to write the imported products' copy. */\nexport const AI_PRODUCTS_IMPORT_WRITE_COPY = 'ai.writeCopy'\n\nconst BULK_BRIEF = /^Write storefront copy for (\\d+) products?$/\n\n/** How many products a bulk copy job was asked for, read from its brief; `null` for any other job. */\nexport function aiProductsBulkCount(job: Pick<AiJobSummary, 'brief'> | null | undefined): number | null {\n const match = BULK_BRIEF.exec(job?.brief ?? '')\n return match ? Number(match[1]) : null\n}\n"],"names":["AI_PRODUCTS_BULK_MAX","aiProductsBulkBrief","aiProductsBulkInputs","AI_PRODUCTS_TARGETS","AI_PRODUCT_TYPES","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_TEXT_MAX_CHARS","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","AI_PRODUCT_OPTIONS_MAX","AI_PRODUCT_OPTION_VALUES_MAX","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_CATALOG_PRODUCTS","min","max","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_TAGS_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRICE_USD","aiCatalogPrice","raw","value","match","exec","Number","isFinite","cents","Math","round","AI_CATEGORIES_MAX","AI_DISCOUNTS_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_PROPOSAL_WHY_MAX_CHARS","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_CODE","aiProductMerchantWords","product","name","text","tags","join","ID","replace","trim","slice","jsonStrings","maxChars","parsed","JSON","parse","Array","isArray","map","entry","filter","Boolean","jsonOptions","record","values","AI_PRODUCTS_INPUTS_MAX_JSON_CHARS","aiProductsJobInputs","input","target","inputs","productId","id","type","stringify","tag","categoryIds","test","options","option","imageUrl","seoTitle","seoDescription","length","productIds","AI_PRODUCTS_NO_TARGET_COPY","AI_PRODUCTS_NO_NAME_COPY","AI_PRODUCTS_NO_SELECTION_COPY","AI_PRODUCTS_TOO_MANY_COPY","parseAiProductsJobInputs","includes","String","split","part","ids","unique","Set","AI_CATALOG_NAME_REASK","AI_CATALOG_NAME_CODE","KIND_HEAD","BRANDS","MODEL_CODE","aiTextNamesBrand","brief","brand","toLowerCase","aiCatalogNameProblem","plain","words","model","AI_PRODUCTS_OUTPUT_IDS","copy","catalog","categories","isRecord","aiProductsProposalOf","output","resource","proposal","kind","aiProductCopyProposals","job","outputs","aiProductsProposalOfKind","aiProductGaps","texts","found","matchAll","add","AI_PRODUCTS_IMPORT_WRITE_COPY","BULK_BRIEF","aiProductsBulkCount"],"mappings":"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"}
@@ -51,8 +51,12 @@ import type { AiSiteInventory } from './ai-site-inventory';
51
51
  export declare const AI_SITE_CONTEXT_MAX_CHARS = 1600;
52
52
  /** Pages, forms, components and layouts listed by name in the site index, each. */
53
53
  export declare const AI_SITE_CONTEXT_INDEX_PER_KIND = 8;
54
- /** The rule every block closes with, whatever else it says. */
55
- export declare const AI_SITE_CONTEXT_RULE = "Never invent a business name, a person\u2019s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, write around it or leave a bracketed placeholder such as [phone number] for the owner to fill.";
54
+ /**
55
+ * The rule every block closes with, whatever else it says. A missing fact is
56
+ * left out, never a gap for the owner (AGL-4092): "[phone number]" here put
57
+ * "[email]" and "[phone]" in an Assist build's plan.
58
+ */
59
+ export declare const AI_SITE_CONTEXT_RULE = "Never invent a business name, a person\u2019s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, leave it out and write the sentence without it, never a placeholder for the owner to fill.";
56
60
  export interface AiSiteContextInput {
57
61
  /** The resolved business profile; `null` when the site has none to read. */
58
62
  profile: ResolvedBusinessProfile | null;
@@ -45,7 +45,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
45
45
  * dropped, because it is the line that stops an invented phone number.
46
46
  */ /** The block's ceiling: 400 tokens at four characters a token. */ export const AI_SITE_CONTEXT_MAX_CHARS = 1600;
47
47
  /** Pages, forms, components and layouts listed by name in the site index, each. */ export const AI_SITE_CONTEXT_INDEX_PER_KIND = 8;
48
- /** The rule every block closes with, whatever else it says. */ export const AI_SITE_CONTEXT_RULE = 'Never invent a business name, a person’s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, write around it or leave a bracketed placeholder such as [phone number] for the owner to fill.';
48
+ /**
49
+ * The rule every block closes with, whatever else it says. A missing fact is
50
+ * left out, never a gap for the owner (AGL-4092): "[phone number]" here put
51
+ * "[email]" and "[phone]" in an Assist build's plan.
52
+ */ export const AI_SITE_CONTEXT_RULE = 'Never invent a business name, a person’s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, leave it out and write the sentence without it, never a placeholder for the owner to fill.';
49
53
  const clip = (value, max)=>value.length > max ? `${value.slice(0, max - 1)}…` : value;
50
54
  /** One labelled line, or nothing for an empty value. */ const line = (label, value, max = 240)=>value && value.trim() ? `${label}: ${clip(value.trim(), max)}` : null;
51
55
  const pathOf = (slug)=>`/${slug.trim().replace(/^\/+|\/+$/g, '')}`;
@@ -88,13 +92,15 @@ const pathOf = (slug)=>`/${slug.trim().replace(/^\/+|\/+$/g, '')}`;
88
92
  const business = [];
89
93
  if (profile) {
90
94
  var _ref;
91
- var _profile_toneNotes, _profile_name, _profile_whatYouDo, _profile_services, _profile_serviceArea, _profile_audience;
95
+ var _profile_toneNotes, _profile_name, _profile_whatYouDo, _profile_founded, _profile_services, _profile_serviceArea, _profile_audience;
92
96
  const toneLabel = profile.tone ? BUSINESS_TONE_LABELS[profile.tone.value] : null;
93
97
  const toneNotes = (_ref = (_profile_toneNotes = profile.toneNotes) == null ? void 0 : _profile_toneNotes.value) != null ? _ref : '';
94
98
  business.push(...[
95
99
  line('Business name', (_profile_name = profile.name) == null ? void 0 : _profile_name.value, 120),
96
100
  line('What it does', (_profile_whatYouDo = profile.whatYouDo) == null ? void 0 : _profile_whatYouDo.value),
97
101
  line('Business type', profile.businessType, 80),
102
+ // The year the business started (AGL-4092), which a plan once marked "[year founded]".
103
+ line('Founded', (_profile_founded = profile.founded) == null ? void 0 : _profile_founded.value, 12),
98
104
  line('Services', (_profile_services = profile.services) == null ? void 0 : _profile_services.value.join('; ')),
99
105
  line('Area served', (_profile_serviceArea = profile.serviceArea) == null ? void 0 : _profile_serviceArea.value, 160),
100
106
  line('Audience', (_profile_audience = profile.audience) == null ? void 0 : _profile_audience.value, 160),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-context.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 BUSINESS_TONE_LABELS,\n type ResolvedBusinessProfile,\n} from '@aglyn/aglyn/app-utils/business-profile'\nimport type { AiSystemBlock } from '../providers/contract'\nimport type { AiHostPublishContext } from './ai-host-publish-context'\nimport type { AiSiteInventory } from './ai-site-inventory'\n\n/**\n * What every AI job knows about the site it works on (AGL-3661), as ONE\n * compact block: the business profile, what the site already has and whether\n * it is live, and what the owner's edits taught it.\n *\n * ── Why it exists ─────────────────────────────────────────────────────────\n *\n * Before it, a job saw its brief, the guided start's answers and the site's\n * name. It named a Free site \"Austin Paws Grooming\" that was called nothing\n * of the kind, and nothing stopped it inventing a phone number. The block\n * gives it the facts the owner entered and a standing rule for the ones they\n * did not: never invent a name, a contact detail, a price or a review.\n *\n * ── Where it sits, and what that costs ────────────────────────────────────\n *\n * {@link aiSiteContextSystemBlock} marks it a `cacheBreakpoint`, placed AFTER\n * every platform-wide cached block and before the volatile ones. The\n * platform's prefix is still shared by every workspace; this block extends it\n * with one cache entry per site, which every request of a job — each re-ask,\n * each section of a page — reads at the cache rate. It changes only when the\n * owner edits the profile, a page is published, or an edit is remembered.\n *\n * ── Held to a ceiling ─────────────────────────────────────────────────────\n *\n * {@link AI_SITE_CONTEXT_MAX_CHARS}: 400 tokens at the runtime's four\n * characters a token. Lines are dropped from the least useful up — the site\n * index, the profiles, then the preferences — and the closing rule is never\n * dropped, because it is the line that stops an invented phone number.\n */\n\n/** The block's ceiling: 400 tokens at four characters a token. */\nexport const AI_SITE_CONTEXT_MAX_CHARS = 1_600\n\n/** Pages, forms, components and layouts listed by name in the site index, each. */\nexport const AI_SITE_CONTEXT_INDEX_PER_KIND = 8\n\n/** The rule every block closes with, whatever else it says. */\nexport const AI_SITE_CONTEXT_RULE =\n 'Never invent a business name, a person’s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, write around it or leave a bracketed placeholder such as [phone number] for the owner to fill.'\n\nexport interface AiSiteContextInput {\n /** The resolved business profile; `null` when the site has none to read. */\n profile: ResolvedBusinessProfile | null\n /**\n * The site's inventory, for a caller that does NOT already send it. A job\n * whose prompt carries the inventory block (the plan step and the writers\n * built on the doctrine) leaves this out: the block would only repeat it.\n */\n inventory?: AiSiteInventory | null\n /** Whether the site is live and where. */\n publish?: AiHostPublishContext | null\n /** What the owner's edits taught, strongest first. */\n preferences?: readonly string[]\n /**\n * Whether the block may carry contact details. An answer that must name\n * nobody and print no address (the insight rules) leaves them out.\n */\n contact?: boolean\n}\n\nconst clip = (value: string, max: number) => (value.length > max ? `${value.slice(0, max - 1)}…` : value)\n\n/** One labelled line, or nothing for an empty value. */\nconst line = (label: string, value: string | null | undefined, max = 240): string | null =>\n value && value.trim() ? `${label}: ${clip(value.trim(), max)}` : null\n\nconst pathOf = (slug: string) => `/${slug.trim().replace(/^\\/+|\\/+$/g, '')}`\n\n/**\n * The site's index (AGL-3661): what it already has, by name and id, so a job\n * reuses before it builds. Pages say whether they are live; a form says how\n * many fields it asks for. Pure, so a caller holding an inventory pays one\n * render and no read.\n */\nexport function aiSiteInventoryIndex(\n inventory: AiSiteInventory | null | undefined,\n publish?: AiHostPublishContext | null,\n): string[] {\n if (!inventory) return []\n const live = new Set((publish?.publishedPages ?? []).map((path) => pathOf(path)))\n const cap = AI_SITE_CONTEXT_INDEX_PER_KIND\n const more = (total: number) => (total > cap ? `, and ${total - cap} more` : '')\n const lines: string[] = []\n const pages = inventory.screens.filter((screen) => !screen.template)\n if (pages.length) {\n const rows = pages.slice(0, cap).map((page) => {\n const path = pathOf(page.slug)\n return `${page.name} ${path} (${live.has(path) ? 'live' : 'draft'})`\n })\n lines.push(`Pages: ${rows.join('; ')}${more(pages.length)}`)\n }\n if (inventory.forms.length) {\n const rows = inventory.forms.slice(0, cap).map((form) => `${form.name} [${form.id}, ${form.fields.length} fields]`)\n lines.push(`Forms: ${rows.join('; ')}${more(inventory.forms.length)}`)\n }\n if (inventory.components.length) {\n const rows = inventory.components.slice(0, cap).map((component) => `${component.name} [${component.id}]`)\n lines.push(`Components: ${rows.join('; ')}${more(inventory.components.length)}`)\n }\n if (inventory.layouts.length) {\n const rows = inventory.layouts.slice(0, cap).map((layout) => `${layout.name} [${layout.id}]`)\n lines.push(`Layouts: ${rows.join('; ')}${more(inventory.layouts.length)}`)\n }\n return lines\n}\n\n/** The block's lines in priority order; each group is dropped whole when it does not fit. */\nfunction contextGroups(input: AiSiteContextInput): string[][] {\n const { profile, publish, preferences } = input\n const business: string[] = []\n if (profile) {\n const toneLabel = profile.tone ? BUSINESS_TONE_LABELS[profile.tone.value] : null\n const toneNotes = profile.toneNotes?.value ?? ''\n business.push(\n ...[\n line('Business name', profile.name?.value, 120),\n line('What it does', profile.whatYouDo?.value),\n line('Business type', profile.businessType, 80),\n line('Services', profile.services?.value.join('; ')),\n line('Area served', profile.serviceArea?.value, 160),\n line('Audience', profile.audience?.value, 160),\n line('Tone of voice', [toneLabel, toneNotes].filter(Boolean).join(' — ') || null),\n ].filter((entry): entry is string => Boolean(entry)),\n )\n }\n const contact: string[] = []\n if (profile && input.contact !== false) {\n const parts = [\n profile.contact.email ? `email ${profile.contact.email}` : null,\n profile.contact.phone ? `phone ${profile.contact.phone}` : null,\n profile.contact.address ? `address ${profile.contact.address}` : null,\n profile.contact.hours ? `hours ${profile.contact.hours}` : null,\n ].filter(Boolean)\n contact.push(\n parts.length\n ? `Contact details the owner entered (use exactly as written): ${clip(parts.join('; '), 400)}`\n : 'Contact details: none entered yet.',\n )\n }\n const status: string[] = []\n if (publish) {\n status.push(\n publish.published\n ? `Site: live at ${publish.liveUrl ?? 'its address'}; live pages ${clip(publish.publishedPages.join(', '), 200)}`\n : `Site: not published yet${publish.liveUrl ? `; it will be at ${publish.liveUrl}` : ''}`,\n )\n }\n const remembered = preferences?.length\n ? [`What the owner's past edits showed: ${clip(preferences.join('; '), 360)}.`]\n : []\n const profiles =\n profile && input.contact !== false && profile.profiles.length\n ? [`Social profiles: ${clip(profile.profiles.join(', '), 300)}`]\n : []\n const index = aiSiteInventoryIndex(input.inventory, publish)\n // Priority order: what the business is, how to reach it, whether it is\n // live, what the owner taught, its profiles, then what the site holds.\n return [business, contact, status, remembered, profiles, index]\n}\n\n/**\n * The block as text (AGL-3661), never over {@link AI_SITE_CONTEXT_MAX_CHARS}.\n * `''` when there is nothing to say, so a caller sends no block at all.\n */\nexport function aiSiteContextBlock(input: AiSiteContextInput): string {\n const groups = contextGroups(input).filter((group) => group.length)\n if (!groups.length) return ''\n const head = 'About this site, from its owner’s settings. Write for this business and do not contradict these facts:'\n const render = (kept: string[][]) => [head, ...kept.flat(), AI_SITE_CONTEXT_RULE].join('\\n')\n const kept = [...groups]\n let text = render(kept)\n while (text.length > AI_SITE_CONTEXT_MAX_CHARS && kept.length > 1) {\n kept.pop()\n text = render(kept)\n }\n if (text.length <= AI_SITE_CONTEXT_MAX_CHARS) return text\n // One group still too long (a very long profile): its lines are cut, the\n // closing rule is kept whole.\n const room = AI_SITE_CONTEXT_MAX_CHARS - head.length - AI_SITE_CONTEXT_RULE.length - 2\n return [head, clip(kept.flat().join('\\n'), Math.max(0, room)), AI_SITE_CONTEXT_RULE].join('\\n')\n}\n\n/**\n * The block as the system block a door appends after its own cached blocks\n * (AGL-3661): a `cacheBreakpoint` of its own, so the platform's prefix ahead\n * of it is still shared and this one is cached per site. `[]` when there is\n * nothing to say.\n *\n * A door already carrying the runtime's limit of breakpoints passes\n * `cache: false`, and the block rides as a volatile block after them instead.\n */\nexport function aiSiteContextSystemBlock(\n input: AiSiteContextInput | null | undefined,\n options: { cache?: boolean } = {},\n): AiSystemBlock[] {\n const text = input ? aiSiteContextBlock(input) : ''\n if (!text) return []\n return [options.cache === false ? { text, volatile: true, site: true } : { text, cacheBreakpoint: true, site: true }]\n}\n\n/**\n * A door's own instructions with the site context after them (AGL-3661) —\n * THE call a generator makes to give its model the site:\n *\n * instructions: aiInstructionsWithSiteContext(MY_INSTRUCTIONS, context)\n *\n * The door's last instruction block is marked a breakpoint, so the\n * platform-wide prefix (the doctrine and the door's rules) stays one cache\n * entry for every workspace, and the context block after it is the per-site\n * entry. Without a context it hands the instructions back unchanged, so a\n * door's prompt is byte-for-byte what it was for a job that has no site.\n */\nexport function aiInstructionsWithSiteContext(\n instructions: readonly AiSystemBlock[],\n context: AiSiteContextInput | null | undefined,\n): readonly AiSystemBlock[] {\n const site = aiSiteContextSystemBlock(context)\n if (!site.length) return instructions\n const blocks = [...instructions]\n const last = blocks.length - 1\n if (last >= 0 && !blocks[last].volatile && !blocks.some((block) => block.cacheBreakpoint)) {\n blocks[last] = { ...blocks[last], cacheBreakpoint: true }\n }\n return [...blocks, ...site]\n}\n"],"names":["BUSINESS_TONE_LABELS","AI_SITE_CONTEXT_MAX_CHARS","AI_SITE_CONTEXT_INDEX_PER_KIND","AI_SITE_CONTEXT_RULE","clip","value","max","length","slice","line","label","trim","pathOf","slug","replace","aiSiteInventoryIndex","inventory","publish","live","Set","publishedPages","map","path","cap","more","total","lines","pages","screens","filter","screen","template","rows","page","name","has","push","join","forms","form","id","fields","components","component","layouts","layout","contextGroups","input","profile","preferences","business","toneLabel","tone","toneNotes","whatYouDo","businessType","services","serviceArea","audience","Boolean","entry","contact","parts","email","phone","address","hours","status","published","liveUrl","remembered","profiles","index","aiSiteContextBlock","groups","group","head","render","kept","flat","text","pop","room","Math","aiSiteContextSystemBlock","options","cache","volatile","site","cacheBreakpoint","aiInstructionsWithSiteContext","instructions","context","blocks","last","some","block"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-context.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 BUSINESS_TONE_LABELS,\n type ResolvedBusinessProfile,\n} from '@aglyn/aglyn/app-utils/business-profile'\nimport type { AiSystemBlock } from '../providers/contract'\nimport type { AiHostPublishContext } from './ai-host-publish-context'\nimport type { AiSiteInventory } from './ai-site-inventory'\n\n/**\n * What every AI job knows about the site it works on (AGL-3661), as ONE\n * compact block: the business profile, what the site already has and whether\n * it is live, and what the owner's edits taught it.\n *\n * ── Why it exists ─────────────────────────────────────────────────────────\n *\n * Before it, a job saw its brief, the guided start's answers and the site's\n * name. It named a Free site \"Austin Paws Grooming\" that was called nothing\n * of the kind, and nothing stopped it inventing a phone number. The block\n * gives it the facts the owner entered and a standing rule for the ones they\n * did not: never invent a name, a contact detail, a price or a review.\n *\n * ── Where it sits, and what that costs ────────────────────────────────────\n *\n * {@link aiSiteContextSystemBlock} marks it a `cacheBreakpoint`, placed AFTER\n * every platform-wide cached block and before the volatile ones. The\n * platform's prefix is still shared by every workspace; this block extends it\n * with one cache entry per site, which every request of a job — each re-ask,\n * each section of a page — reads at the cache rate. It changes only when the\n * owner edits the profile, a page is published, or an edit is remembered.\n *\n * ── Held to a ceiling ─────────────────────────────────────────────────────\n *\n * {@link AI_SITE_CONTEXT_MAX_CHARS}: 400 tokens at the runtime's four\n * characters a token. Lines are dropped from the least useful up — the site\n * index, the profiles, then the preferences — and the closing rule is never\n * dropped, because it is the line that stops an invented phone number.\n */\n\n/** The block's ceiling: 400 tokens at four characters a token. */\nexport const AI_SITE_CONTEXT_MAX_CHARS = 1_600\n\n/** Pages, forms, components and layouts listed by name in the site index, each. */\nexport const AI_SITE_CONTEXT_INDEX_PER_KIND = 8\n\n/**\n * The rule every block closes with, whatever else it says. A missing fact is\n * left out, never a gap for the owner (AGL-4092): \"[phone number]\" here put\n * \"[email]\" and \"[phone]\" in an Assist build's plan.\n */\nexport const AI_SITE_CONTEXT_RULE =\n 'Never invent a business name, a person’s name, an email address, a phone number, a street address, opening hours, prices, reviews or testimonials. Use only the facts above; where a page needs one that is missing, leave it out and write the sentence without it, never a placeholder for the owner to fill.'\n\nexport interface AiSiteContextInput {\n /** The resolved business profile; `null` when the site has none to read. */\n profile: ResolvedBusinessProfile | null\n /**\n * The site's inventory, for a caller that does NOT already send it. A job\n * whose prompt carries the inventory block (the plan step and the writers\n * built on the doctrine) leaves this out: the block would only repeat it.\n */\n inventory?: AiSiteInventory | null\n /** Whether the site is live and where. */\n publish?: AiHostPublishContext | null\n /** What the owner's edits taught, strongest first. */\n preferences?: readonly string[]\n /**\n * Whether the block may carry contact details. An answer that must name\n * nobody and print no address (the insight rules) leaves them out.\n */\n contact?: boolean\n}\n\nconst clip = (value: string, max: number) => (value.length > max ? `${value.slice(0, max - 1)}…` : value)\n\n/** One labelled line, or nothing for an empty value. */\nconst line = (label: string, value: string | null | undefined, max = 240): string | null =>\n value && value.trim() ? `${label}: ${clip(value.trim(), max)}` : null\n\nconst pathOf = (slug: string) => `/${slug.trim().replace(/^\\/+|\\/+$/g, '')}`\n\n/**\n * The site's index (AGL-3661): what it already has, by name and id, so a job\n * reuses before it builds. Pages say whether they are live; a form says how\n * many fields it asks for. Pure, so a caller holding an inventory pays one\n * render and no read.\n */\nexport function aiSiteInventoryIndex(\n inventory: AiSiteInventory | null | undefined,\n publish?: AiHostPublishContext | null,\n): string[] {\n if (!inventory) return []\n const live = new Set((publish?.publishedPages ?? []).map((path) => pathOf(path)))\n const cap = AI_SITE_CONTEXT_INDEX_PER_KIND\n const more = (total: number) => (total > cap ? `, and ${total - cap} more` : '')\n const lines: string[] = []\n const pages = inventory.screens.filter((screen) => !screen.template)\n if (pages.length) {\n const rows = pages.slice(0, cap).map((page) => {\n const path = pathOf(page.slug)\n return `${page.name} ${path} (${live.has(path) ? 'live' : 'draft'})`\n })\n lines.push(`Pages: ${rows.join('; ')}${more(pages.length)}`)\n }\n if (inventory.forms.length) {\n const rows = inventory.forms.slice(0, cap).map((form) => `${form.name} [${form.id}, ${form.fields.length} fields]`)\n lines.push(`Forms: ${rows.join('; ')}${more(inventory.forms.length)}`)\n }\n if (inventory.components.length) {\n const rows = inventory.components.slice(0, cap).map((component) => `${component.name} [${component.id}]`)\n lines.push(`Components: ${rows.join('; ')}${more(inventory.components.length)}`)\n }\n if (inventory.layouts.length) {\n const rows = inventory.layouts.slice(0, cap).map((layout) => `${layout.name} [${layout.id}]`)\n lines.push(`Layouts: ${rows.join('; ')}${more(inventory.layouts.length)}`)\n }\n return lines\n}\n\n/** The block's lines in priority order; each group is dropped whole when it does not fit. */\nfunction contextGroups(input: AiSiteContextInput): string[][] {\n const { profile, publish, preferences } = input\n const business: string[] = []\n if (profile) {\n const toneLabel = profile.tone ? BUSINESS_TONE_LABELS[profile.tone.value] : null\n const toneNotes = profile.toneNotes?.value ?? ''\n business.push(\n ...[\n line('Business name', profile.name?.value, 120),\n line('What it does', profile.whatYouDo?.value),\n line('Business type', profile.businessType, 80),\n // The year the business started (AGL-4092), which a plan once marked \"[year founded]\".\n line('Founded', profile.founded?.value, 12),\n line('Services', profile.services?.value.join('; ')),\n line('Area served', profile.serviceArea?.value, 160),\n line('Audience', profile.audience?.value, 160),\n line('Tone of voice', [toneLabel, toneNotes].filter(Boolean).join(' — ') || null),\n ].filter((entry): entry is string => Boolean(entry)),\n )\n }\n const contact: string[] = []\n if (profile && input.contact !== false) {\n const parts = [\n profile.contact.email ? `email ${profile.contact.email}` : null,\n profile.contact.phone ? `phone ${profile.contact.phone}` : null,\n profile.contact.address ? `address ${profile.contact.address}` : null,\n profile.contact.hours ? `hours ${profile.contact.hours}` : null,\n ].filter(Boolean)\n contact.push(\n parts.length\n ? `Contact details the owner entered (use exactly as written): ${clip(parts.join('; '), 400)}`\n : 'Contact details: none entered yet.',\n )\n }\n const status: string[] = []\n if (publish) {\n status.push(\n publish.published\n ? `Site: live at ${publish.liveUrl ?? 'its address'}; live pages ${clip(publish.publishedPages.join(', '), 200)}`\n : `Site: not published yet${publish.liveUrl ? `; it will be at ${publish.liveUrl}` : ''}`,\n )\n }\n const remembered = preferences?.length\n ? [`What the owner's past edits showed: ${clip(preferences.join('; '), 360)}.`]\n : []\n const profiles =\n profile && input.contact !== false && profile.profiles.length\n ? [`Social profiles: ${clip(profile.profiles.join(', '), 300)}`]\n : []\n const index = aiSiteInventoryIndex(input.inventory, publish)\n // Priority order: what the business is, how to reach it, whether it is\n // live, what the owner taught, its profiles, then what the site holds.\n return [business, contact, status, remembered, profiles, index]\n}\n\n/**\n * The block as text (AGL-3661), never over {@link AI_SITE_CONTEXT_MAX_CHARS}.\n * `''` when there is nothing to say, so a caller sends no block at all.\n */\nexport function aiSiteContextBlock(input: AiSiteContextInput): string {\n const groups = contextGroups(input).filter((group) => group.length)\n if (!groups.length) return ''\n const head = 'About this site, from its owner’s settings. Write for this business and do not contradict these facts:'\n const render = (kept: string[][]) => [head, ...kept.flat(), AI_SITE_CONTEXT_RULE].join('\\n')\n const kept = [...groups]\n let text = render(kept)\n while (text.length > AI_SITE_CONTEXT_MAX_CHARS && kept.length > 1) {\n kept.pop()\n text = render(kept)\n }\n if (text.length <= AI_SITE_CONTEXT_MAX_CHARS) return text\n // One group still too long (a very long profile): its lines are cut, the\n // closing rule is kept whole.\n const room = AI_SITE_CONTEXT_MAX_CHARS - head.length - AI_SITE_CONTEXT_RULE.length - 2\n return [head, clip(kept.flat().join('\\n'), Math.max(0, room)), AI_SITE_CONTEXT_RULE].join('\\n')\n}\n\n/**\n * The block as the system block a door appends after its own cached blocks\n * (AGL-3661): a `cacheBreakpoint` of its own, so the platform's prefix ahead\n * of it is still shared and this one is cached per site. `[]` when there is\n * nothing to say.\n *\n * A door already carrying the runtime's limit of breakpoints passes\n * `cache: false`, and the block rides as a volatile block after them instead.\n */\nexport function aiSiteContextSystemBlock(\n input: AiSiteContextInput | null | undefined,\n options: { cache?: boolean } = {},\n): AiSystemBlock[] {\n const text = input ? aiSiteContextBlock(input) : ''\n if (!text) return []\n return [options.cache === false ? { text, volatile: true, site: true } : { text, cacheBreakpoint: true, site: true }]\n}\n\n/**\n * A door's own instructions with the site context after them (AGL-3661) —\n * THE call a generator makes to give its model the site:\n *\n * instructions: aiInstructionsWithSiteContext(MY_INSTRUCTIONS, context)\n *\n * The door's last instruction block is marked a breakpoint, so the\n * platform-wide prefix (the doctrine and the door's rules) stays one cache\n * entry for every workspace, and the context block after it is the per-site\n * entry. Without a context it hands the instructions back unchanged, so a\n * door's prompt is byte-for-byte what it was for a job that has no site.\n */\nexport function aiInstructionsWithSiteContext(\n instructions: readonly AiSystemBlock[],\n context: AiSiteContextInput | null | undefined,\n): readonly AiSystemBlock[] {\n const site = aiSiteContextSystemBlock(context)\n if (!site.length) return instructions\n const blocks = [...instructions]\n const last = blocks.length - 1\n if (last >= 0 && !blocks[last].volatile && !blocks.some((block) => block.cacheBreakpoint)) {\n blocks[last] = { ...blocks[last], cacheBreakpoint: true }\n }\n return [...blocks, ...site]\n}\n"],"names":["BUSINESS_TONE_LABELS","AI_SITE_CONTEXT_MAX_CHARS","AI_SITE_CONTEXT_INDEX_PER_KIND","AI_SITE_CONTEXT_RULE","clip","value","max","length","slice","line","label","trim","pathOf","slug","replace","aiSiteInventoryIndex","inventory","publish","live","Set","publishedPages","map","path","cap","more","total","lines","pages","screens","filter","screen","template","rows","page","name","has","push","join","forms","form","id","fields","components","component","layouts","layout","contextGroups","input","profile","preferences","business","toneLabel","tone","toneNotes","whatYouDo","businessType","founded","services","serviceArea","audience","Boolean","entry","contact","parts","email","phone","address","hours","status","published","liveUrl","remembered","profiles","index","aiSiteContextBlock","groups","group","head","render","kept","flat","text","pop","room","Math","aiSiteContextSystemBlock","options","cache","volatile","site","cacheBreakpoint","aiInstructionsWithSiteContext","instructions","context","blocks","last","some","block"],"mappings":"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"}
@@ -25,8 +25,9 @@
25
25
  * (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo
26
26
  * of theirs: nothing a page says reaches a prompt.
27
27
  *
28
- * Behind `release_ai_site_inspiration`, off by default: with it off the
29
- * guided start asks for no site and a job's inspiration is never read.
28
+ * Behind `release_ai_site_inspiration`, on by default since 2026-10-28:
29
+ * turned off, the guided start asks for no site and a job's inspiration is
30
+ * never read.
30
31
  *
31
32
  * Pure and console-safe: the input's name, the limits and how an answer is
32
33
  * read. No network.
@@ -24,8 +24,9 @@
24
24
  * (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo
25
25
  * of theirs: nothing a page says reaches a prompt.
26
26
  *
27
- * Behind `release_ai_site_inspiration`, off by default: with it off the
28
- * guided start asks for no site and a job's inspiration is never read.
27
+ * Behind `release_ai_site_inspiration`, on by default since 2026-10-28:
28
+ * turned off, the guided start asks for no site and a job's inspiration is
29
+ * never read.
29
30
  *
30
31
  * Pure and console-safe: the input's name, the limits and how an answer is
31
32
  * read. No network.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inspiration.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 * Sites I like (AGL-3975, part of AGL-3973): the sites a person names in the\n * guided start as inspiration, as the job carries them.\n *\n * Free names one site and a paid plan three. From each the site job takes\n * SIGNALS only — how the sections run, the colors, the type's mood, the shape\n * of buttons and cards, how the pages move and, on a paid plan, which\n * features it offers — each read by code into the site job's own closed sets\n * (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo\n * of theirs: nothing a page says reaches a prompt.\n *\n * Behind `release_ai_site_inspiration`, off by default: with it off the\n * guided start asks for no site and a job's inspiration is never read.\n *\n * Pure and console-safe: the input's name, the limits and how an answer is\n * read. No network.\n */\n\n/** The release flag the whole feature waits behind. */\nexport const AI_SITE_INSPIRATION_FLAG = 'release_ai_site_inspiration' as const\n\n/**\n * The job input the sites travel in: one string, the addresses separated by\n * spaces, because a job's inputs are scalars (`parseCreateAiJobBody`).\n */\nexport const AI_SITE_INSPIRATION_INPUT = 'inspiration'\n\n/** How many sites a plan may name: Free one, every paid plan three. */\nexport const AI_SITE_INSPIRATION_LIMITS = { free: 1, paid: 3 } as const\n\n/** The most any job may carry, whatever its plan. */\nexport const AI_SITE_INSPIRATION_MAX = AI_SITE_INSPIRATION_LIMITS.paid\n\n/** The longest address one site may be given as. */\nexport const AI_SITE_INSPIRATION_URL_MAX_CHARS = 300\n\n/** What the guided start says a paid plan adds, beside a Free workspace's locked fields. */\nexport const AI_SITE_INSPIRATION_PAID_NOTE = `Paid plans can name up to ${AI_SITE_INSPIRATION_LIMITS.paid} sites, and also match the features they offer, like booking, a store or a blog.`\n\n/** How many sites a workspace may name. */\nexport function aiSiteInspirationLimit(freeTaste: boolean | null | undefined): number {\n return freeTaste ? AI_SITE_INSPIRATION_LIMITS.free : AI_SITE_INSPIRATION_LIMITS.paid\n}\n\n/**\n * One address as the job keeps it: https only, no credentials, the default\n * port, no fragment. `null` for anything else. A bare host (\"example.com\")\n * is read as its https address, the way a person types one.\n */\nexport function aiSiteInspirationUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed || trimmed.length > AI_SITE_INSPIRATION_URL_MAX_CHARS) return null\n const written = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`\n let url: URL\n try {\n url = new URL(written)\n } catch {\n return null\n }\n if (url.protocol !== 'https:' || url.username || url.password) return null\n if (url.port && url.port !== '443') return null\n // A host with no dot is a typo or an internal name, never a public site.\n if (!url.hostname.includes('.') || url.hostname.endsWith('.')) return null\n url.hash = ''\n return url.toString()\n}\n\n/**\n * The sites a job's inputs name, or the sentence the door refuses them with.\n * Absent or empty is none. Each must be an https address; more than\n * {@link AI_SITE_INSPIRATION_MAX} is refused, and the same site twice counts\n * once. The plan's own limit is the door's (`aiSiteInspirationRefusal`).\n */\nexport function parseAiSiteInspiration(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] | string {\n const raw = inputs?.[AI_SITE_INSPIRATION_INPUT]\n if (raw === undefined || raw === null || raw === '') return []\n if (typeof raw !== 'string') {\n return `${AI_SITE_INSPIRATION_INPUT} must be text: the addresses of the sites you like, separated by spaces`\n }\n const urls: string[] = []\n for (const word of raw.split(/\\s+/).filter(Boolean)) {\n const url = aiSiteInspirationUrl(word)\n if (!url) return 'Give each site you like as its https address, like https://example.com.'\n if (!urls.includes(url)) urls.push(url)\n }\n if (urls.length > AI_SITE_INSPIRATION_MAX) return `Name at most ${AI_SITE_INSPIRATION_MAX} sites you like.`\n return urls\n}\n\n/** Why a workspace may not name this many sites, in a sentence a member reads; `null` when it may. */\nexport function aiSiteInspirationRefusal(urls: readonly string[], freeTaste: boolean | null | undefined): string | null {\n const limit = aiSiteInspirationLimit(freeTaste)\n if (urls.length <= limit) return null\n return freeTaste\n ? `A Free workspace can name ${AI_SITE_INSPIRATION_LIMITS.free} site you like. ${AI_SITE_INSPIRATION_PAID_NOTE}`\n : `Name at most ${limit} sites you like.`\n}\n\n/** The sites as the job input carries them; empty for none, which the start leaves out of its inputs. */\nexport function aiSiteInspirationInput(urls: readonly string[]): string {\n const kept: string[] = []\n for (const url of urls.map(aiSiteInspirationUrl)) {\n if (url && !kept.includes(url)) kept.push(url)\n }\n return kept.slice(0, AI_SITE_INSPIRATION_MAX).join(' ')\n}\n\n/** A site as the review names it: its host, without `www.`. */\nexport function aiSiteInspirationHost(url: string): string {\n try {\n return new URL(url).hostname.replace(/^www\\./, '')\n } catch {\n return url\n }\n}\n"],"names":["AI_SITE_INSPIRATION_FLAG","AI_SITE_INSPIRATION_INPUT","AI_SITE_INSPIRATION_LIMITS","free","paid","AI_SITE_INSPIRATION_MAX","AI_SITE_INSPIRATION_URL_MAX_CHARS","AI_SITE_INSPIRATION_PAID_NOTE","aiSiteInspirationLimit","freeTaste","aiSiteInspirationUrl","value","trimmed","trim","length","written","test","url","URL","protocol","username","password","port","hostname","includes","endsWith","hash","toString","parseAiSiteInspiration","inputs","raw","undefined","urls","word","split","filter","Boolean","push","aiSiteInspirationRefusal","limit","aiSiteInspirationInput","kept","map","slice","join","aiSiteInspirationHost","replace"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inspiration.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 * Sites I like (AGL-3975, part of AGL-3973): the sites a person names in the\n * guided start as inspiration, as the job carries them.\n *\n * Free names one site and a paid plan three. From each the site job takes\n * SIGNALS only — how the sections run, the colors, the type's mood, the shape\n * of buttons and cards, how the pages move and, on a paid plan, which\n * features it offers — each read by code into the site job's own closed sets\n * (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo\n * of theirs: nothing a page says reaches a prompt.\n *\n * Behind `release_ai_site_inspiration`, on by default since 2026-10-28:\n * turned off, the guided start asks for no site and a job's inspiration is\n * never read.\n *\n * Pure and console-safe: the input's name, the limits and how an answer is\n * read. No network.\n */\n\n/** The release flag the whole feature waits behind. */\nexport const AI_SITE_INSPIRATION_FLAG = 'release_ai_site_inspiration' as const\n\n/**\n * The job input the sites travel in: one string, the addresses separated by\n * spaces, because a job's inputs are scalars (`parseCreateAiJobBody`).\n */\nexport const AI_SITE_INSPIRATION_INPUT = 'inspiration'\n\n/** How many sites a plan may name: Free one, every paid plan three. */\nexport const AI_SITE_INSPIRATION_LIMITS = { free: 1, paid: 3 } as const\n\n/** The most any job may carry, whatever its plan. */\nexport const AI_SITE_INSPIRATION_MAX = AI_SITE_INSPIRATION_LIMITS.paid\n\n/** The longest address one site may be given as. */\nexport const AI_SITE_INSPIRATION_URL_MAX_CHARS = 300\n\n/** What the guided start says a paid plan adds, beside a Free workspace's locked fields. */\nexport const AI_SITE_INSPIRATION_PAID_NOTE = `Paid plans can name up to ${AI_SITE_INSPIRATION_LIMITS.paid} sites, and also match the features they offer, like booking, a store or a blog.`\n\n/** How many sites a workspace may name. */\nexport function aiSiteInspirationLimit(freeTaste: boolean | null | undefined): number {\n return freeTaste ? AI_SITE_INSPIRATION_LIMITS.free : AI_SITE_INSPIRATION_LIMITS.paid\n}\n\n/**\n * One address as the job keeps it: https only, no credentials, the default\n * port, no fragment. `null` for anything else. A bare host (\"example.com\")\n * is read as its https address, the way a person types one.\n */\nexport function aiSiteInspirationUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed || trimmed.length > AI_SITE_INSPIRATION_URL_MAX_CHARS) return null\n const written = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`\n let url: URL\n try {\n url = new URL(written)\n } catch {\n return null\n }\n if (url.protocol !== 'https:' || url.username || url.password) return null\n if (url.port && url.port !== '443') return null\n // A host with no dot is a typo or an internal name, never a public site.\n if (!url.hostname.includes('.') || url.hostname.endsWith('.')) return null\n url.hash = ''\n return url.toString()\n}\n\n/**\n * The sites a job's inputs name, or the sentence the door refuses them with.\n * Absent or empty is none. Each must be an https address; more than\n * {@link AI_SITE_INSPIRATION_MAX} is refused, and the same site twice counts\n * once. The plan's own limit is the door's (`aiSiteInspirationRefusal`).\n */\nexport function parseAiSiteInspiration(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): string[] | string {\n const raw = inputs?.[AI_SITE_INSPIRATION_INPUT]\n if (raw === undefined || raw === null || raw === '') return []\n if (typeof raw !== 'string') {\n return `${AI_SITE_INSPIRATION_INPUT} must be text: the addresses of the sites you like, separated by spaces`\n }\n const urls: string[] = []\n for (const word of raw.split(/\\s+/).filter(Boolean)) {\n const url = aiSiteInspirationUrl(word)\n if (!url) return 'Give each site you like as its https address, like https://example.com.'\n if (!urls.includes(url)) urls.push(url)\n }\n if (urls.length > AI_SITE_INSPIRATION_MAX) return `Name at most ${AI_SITE_INSPIRATION_MAX} sites you like.`\n return urls\n}\n\n/** Why a workspace may not name this many sites, in a sentence a member reads; `null` when it may. */\nexport function aiSiteInspirationRefusal(urls: readonly string[], freeTaste: boolean | null | undefined): string | null {\n const limit = aiSiteInspirationLimit(freeTaste)\n if (urls.length <= limit) return null\n return freeTaste\n ? `A Free workspace can name ${AI_SITE_INSPIRATION_LIMITS.free} site you like. ${AI_SITE_INSPIRATION_PAID_NOTE}`\n : `Name at most ${limit} sites you like.`\n}\n\n/** The sites as the job input carries them; empty for none, which the start leaves out of its inputs. */\nexport function aiSiteInspirationInput(urls: readonly string[]): string {\n const kept: string[] = []\n for (const url of urls.map(aiSiteInspirationUrl)) {\n if (url && !kept.includes(url)) kept.push(url)\n }\n return kept.slice(0, AI_SITE_INSPIRATION_MAX).join(' ')\n}\n\n/** A site as the review names it: its host, without `www.`. */\nexport function aiSiteInspirationHost(url: string): string {\n try {\n return new URL(url).hostname.replace(/^www\\./, '')\n } catch {\n return url\n }\n}\n"],"names":["AI_SITE_INSPIRATION_FLAG","AI_SITE_INSPIRATION_INPUT","AI_SITE_INSPIRATION_LIMITS","free","paid","AI_SITE_INSPIRATION_MAX","AI_SITE_INSPIRATION_URL_MAX_CHARS","AI_SITE_INSPIRATION_PAID_NOTE","aiSiteInspirationLimit","freeTaste","aiSiteInspirationUrl","value","trimmed","trim","length","written","test","url","URL","protocol","username","password","port","hostname","includes","endsWith","hash","toString","parseAiSiteInspiration","inputs","raw","undefined","urls","word","split","filter","Boolean","push","aiSiteInspirationRefusal","limit","aiSiteInspirationInput","kept","map","slice","join","aiSiteInspirationHost","replace"],"mappings":"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"}
package/src/lib/plugin.js CHANGED
@@ -28,7 +28,6 @@ import { lazyWidget } from "./lazy-widget.js";
28
28
  */ /** What a page shows while its code arrives. */ const PAGE_LOADING = createElement(LinearProgress, {
29
29
  'aria-label': 'Loading'
30
30
  });
31
- const AiCollaboratorPermissionsCell = lazyWidget('AiCollaboratorPermissionsCell', ()=>import("./components/ai-collaborator-permissions-column.component.js").then((m)=>m.AiCollaboratorPermissionsCell));
32
31
  const AiSaveAsComponent = lazyWidget('AiSaveAsComponent', ()=>import("./components/ai-save-as-component.component.js").then((m)=>m.AiSaveAsComponent));
33
32
  const AiGenerateSectionControl = lazyWidget('AiGenerateSectionControl', ()=>import("./components/besigner-ai-controls.component.js").then((m)=>m.AiGenerateSectionControl));
34
33
  const AiRewriteControl = lazyWidget('AiRewriteControl', ()=>import("./components/besigner-ai-controls.component.js").then((m)=>m.AiRewriteControl));
@@ -197,18 +196,10 @@ const AiJobsPage = lazyWidget('AiJobsPage', ()=>import("./components/ai-jobs-pag
197
196
  permission: 'billing.view',
198
197
  Component: AiTopUsersCard
199
198
  },
200
- // The collaborators table's AI column (AGL-2927): whether each person
201
- // may open the AI doors on the site, per site, ahead of the columns
202
- // that report what they drew.
203
- {
204
- slot: 'hostMembers',
205
- widgetId: 'ai-collaborator-permissions',
206
- title: 'AI',
207
- column: {
208
- header: 'AI'
209
- },
210
- Component: AiCollaboratorPermissionsCell
211
- },
199
+ // The collaborators table's AI toggles (AGL-2927) are the platform's
200
+ // own per-site Permissions dialog now (AGL-4047), which draws every
201
+ // per-site key, these two included; `/api/ai/host-permissions` stays
202
+ // for the native apps.
212
203
  // The copy assistant's besigner controls (AGL-89, AGL-169): Generate a
213
204
  // section on every editor's toolbar, and Rewrite with AI under the
214
205
  // selected element's fields. Each opens the provider's dialog, and is