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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +7 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +21 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +98 -23
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  16. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  17. package/src/lib/declarations.server.js +12 -0
  18. package/src/lib/declarations.server.js.map +1 -1
  19. package/src/lib/jobs/ai-build-capabilities.js +216 -2
  20. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  21. package/src/lib/jobs/ai-job-build-step.d.ts +3 -1
  22. package/src/lib/jobs/ai-job-build-step.js +124 -18
  23. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  25. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  26. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-form-dataset.d.ts +8 -5
  28. package/src/lib/jobs/ai-job-form-dataset.js +28 -10
  29. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
  30. package/src/lib/jobs/ai-job-form-step.d.ts +20 -4
  31. package/src/lib/jobs/ai-job-form-step.js +98 -28
  32. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-layout-language.d.ts +6 -2
  34. package/src/lib/jobs/ai-job-layout-language.js +29 -5
  35. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  36. package/src/lib/jobs/ai-job-layout-step.js +4 -1
  37. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  38. package/src/lib/jobs/ai-job-media-step.d.ts +239 -0
  39. package/src/lib/jobs/ai-job-media-step.js +610 -0
  40. package/src/lib/jobs/ai-job-media-step.js.map +1 -0
  41. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  42. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  43. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  44. package/src/lib/jobs/ai-job-page-language.d.ts +4 -1
  45. package/src/lib/jobs/ai-job-page-language.js +30 -5
  46. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-page-sections.js +5 -4
  48. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-step.js +3 -2
  50. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  51. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -3
  52. package/src/lib/jobs/ai-job-plan-step.js +87 -20
  53. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  55. package/src/lib/jobs/ai-job-products-step.js +2 -2
  56. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-settings-step.d.ts +268 -0
  58. package/src/lib/jobs/ai-job-settings-step.js +690 -0
  59. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  60. package/src/lib/jobs/ai-job-site-content.d.ts +22 -8
  61. package/src/lib/jobs/ai-job-site-content.js +33 -15
  62. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-datasets.d.ts +3 -1
  64. package/src/lib/jobs/ai-job-site-datasets.js +13 -5
  65. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  66. package/src/lib/jobs/ai-job-site-events.d.ts +148 -0
  67. package/src/lib/jobs/ai-job-site-events.js +313 -0
  68. package/src/lib/jobs/ai-job-site-events.js.map +1 -0
  69. package/src/lib/jobs/ai-job-site-step.d.ts +12 -4
  70. package/src/lib/jobs/ai-job-site-step.js +132 -26
  71. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  72. package/src/lib/jobs/ai-job-tracking-step.d.ts +184 -0
  73. package/src/lib/jobs/ai-job-tracking-step.js +501 -0
  74. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -0
  75. package/src/lib/jobs/ai-layout-stock-photos.d.ts +21 -0
  76. package/src/lib/jobs/ai-layout-stock-photos.js +41 -4
  77. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  78. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  79. package/src/lib/jobs/ai-site-publish.js +202 -56
  80. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  81. package/src/lib/jobs/ai-stock-photo-picker.d.ts +24 -0
  82. package/src/lib/jobs/ai-stock-photo-picker.js +108 -0
  83. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -0
  84. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.d.ts +37 -0
  85. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js +47 -0
  86. package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js.map +1 -0
  87. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.d.ts +21 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js +128 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js.map +1 -0
  90. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -7
  91. package/src/lib/layout-language/ai-layout-compiler.js +62 -42
  92. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  93. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  94. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  95. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  96. package/src/lib/layout-language/ai-layout-frame.d.ts +24 -5
  97. package/src/lib/layout-language/ai-layout-frame.js +93 -20
  98. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  99. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  101. package/src/lib/layout-language/ai-layout-labels.d.ts +70 -0
  102. package/src/lib/layout-language/ai-layout-labels.js +162 -0
  103. package/src/lib/layout-language/ai-layout-labels.js.map +1 -0
  104. package/src/lib/layout-language/ai-layout-links.js +5 -2
  105. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  106. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  107. package/src/lib/layout-language/ai-layout-listings.js +66 -5
  108. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  109. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  110. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  111. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  112. package/src/lib/model/ai-build-event-page.d.ts +70 -0
  113. package/src/lib/model/ai-build-event-page.js +145 -0
  114. package/src/lib/model/ai-build-event-page.js.map +1 -0
  115. package/src/lib/model/ai-build-job.d.ts +44 -0
  116. package/src/lib/model/ai-build-job.js +141 -13
  117. package/src/lib/model/ai-build-job.js.map +1 -1
  118. package/src/lib/model/ai-build-plan.d.ts +39 -3
  119. package/src/lib/model/ai-build-plan.js +59 -5
  120. package/src/lib/model/ai-build-plan.js.map +1 -1
  121. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  122. package/src/lib/model/ai-credit-estimate.js +51 -0
  123. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  124. package/src/lib/model/ai-job-activity.js +6 -1
  125. package/src/lib/model/ai-job-activity.js.map +1 -1
  126. package/src/lib/model/ai-jobs.types.d.ts +18 -2
  127. package/src/lib/model/ai-jobs.types.js +6 -1
  128. package/src/lib/model/ai-jobs.types.js.map +1 -1
  129. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  130. package/src/lib/model/ai-layout-menus.js +518 -0
  131. package/src/lib/model/ai-layout-menus.js.map +1 -0
  132. package/src/lib/model/ai-plan-capabilities.d.ts +8 -0
  133. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  134. package/src/lib/model/ai-products.d.ts +31 -1
  135. package/src/lib/model/ai-products.js +34 -1
  136. package/src/lib/model/ai-products.js.map +1 -1
  137. package/src/lib/model/ai-site-inventory.d.ts +17 -0
  138. package/src/lib/model/ai-site-inventory.js +5 -0
  139. package/src/lib/model/ai-site-inventory.js.map +1 -1
  140. package/src/lib/model/ai-site-job.d.ts +12 -2
  141. package/src/lib/model/ai-site-job.js +39 -12
  142. package/src/lib/model/ai-site-job.js.map +1 -1
  143. package/src/lib/model/ai-site-store-pages.js +1 -1
  144. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  145. package/src/lib/runtime/ai-doctrine-validators.d.ts +78 -3
  146. package/src/lib/runtime/ai-doctrine-validators.js +219 -31
  147. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  148. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  149. package/src/lib/runtime/ai-doctrine.js +16 -7
  150. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  151. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  152. package/src/lib/runtime/ai-eval-live.js +1 -1
  153. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  154. package/src/lib/runtime/ai-palette.d.ts +3 -1
  155. package/src/lib/runtime/ai-palette.generated.js +499 -11
  156. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  157. package/src/lib/runtime/ai-palette.js +5 -3
  158. package/src/lib/runtime/ai-palette.js.map +1 -1
  159. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  160. package/src/lib/runtime/ai-products-generation.js +12 -6
  161. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  162. package/src/lib/runtime/ai-template-examples.js +8 -2
  163. package/src/lib/runtime/ai-template-examples.js.map +1 -1
  164. package/src/lib/runtime/site-inventory.d.ts +12 -0
  165. package/src/lib/runtime/site-inventory.js +29 -1
  166. package/src/lib/runtime/site-inventory.js.map +1 -1
  167. package/src/lib/server.js +20 -0
  168. package/src/lib/server.js.map +1 -1
  169. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  170. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  171. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  172. package/src/lib/tools/ai-products-tool.js +15 -5
  173. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n /**\n * Whether this job's pages are written in the compact layout language\n * (AGL-3660), whose compiler draws a section's repeated items itself: as\n * instances of a component the plan places there, or, with none, in their\n * compact form, a title over its text, which is smaller than a block rule 1\n * calls a repeat. A plan is then not asked for a component for a section's\n * repeated items (AGL-3616); a section two pages share still is one. The\n * plan step sets it for a `build` job. Absent is `false`.\n *\n * It is the plan half of one rule. The page half is the tree check's\n * `AiDoctrineTreeContext.repeatsCompiled`, which every page the layout\n * language compiles is checked with: where the site's design draws a\n * section's items as picture cards — larger than the compact form, and so\n * a block rule 1 would count — those cards are the compiler's, not a block\n * typed out twice, and rule 1 asks no component of them (AGL-3660).\n */\n repeatsCompiled?: boolean\n /**\n * The most datasets this job's plan keeps (AGL-3616): a site start builds\n * at most a few. A plan naming more is settled in code before the rules\n * read it (`aiSettlePlanDatasets`), never re-asked. Absent is no cap beyond\n * `create.dataset.left`.\n */\n datasetsMax?: number\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a build's request states (AGL-3616): its pages are written in\n * the layout language, which draws a section's repeated items itself, so the\n * plan counts them in the section's `items` and needs no component for them.\n */\nexport const AI_PLAN_COMPILED_REPEATS_SENTENCE =\n \"A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_COMPILED_REPEATS_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-plan-capabilities.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_BUILD_PLAN_CREATE_KINDS,\n AI_BUILD_PLAN_CREATION_NOUNS,\n type AiBuildPlan,\n type AiBuildPlanCreateKind,\n} from './ai-build-plan'\n\n/**\n * What a plan may create on one site (AGL-3030): the workspace's plan and the\n * site's counts against its limits, narrowed to what the job itself builds.\n *\n * A plan that names a creation nothing can make is a dead end the member pays\n * for twice: once for the plan, and again for the plan they describe after\n * being told to make the creation by hand — which, on a workspace whose plan\n * does not include it, they cannot. So the plan step is TOLD what may be\n * created before it answers, and the plan rules refuse a creation outside it\n * with the one re-ask every other rule gets.\n *\n * ── Where the doctrine builds inline ─────────────────────────────────────\n *\n * The building doctrine makes a repeat a reusable component and a form a\n * saved form placed by id. A workspace whose plan keeps no reusable\n * components draws a list's repeated items in one section instead\n * (AGL-3071); `reusableComponents` is the one flag every validator reads for\n * that.\n *\n * A form is never drawn inline (AGL-3596): it is a saved form, counted\n * against the site's form allowance like one a member makes. Where the site\n * has none to place and may create none, the page places no form at all.\n *\n * ── Kept out of the cached prefix ────────────────────────────────────────\n *\n * The capabilities are per workspace and per site, so they ride the plan's\n * USER turn, never a system block: the doctrine's cached prefix stays one\n * entry for the platform. The doctrine states the rule once — create only\n * what the request says may be created, and build inline where it says the\n * workspace keeps no reusable components — and the request states the facts.\n *\n * This module imports nothing at runtime beyond the plan's own model, so the\n * validators, the plan step, the doors and their specs read one vocabulary.\n * What a workspace's plan and counts come to is read by\n * `readAiPlanCapabilities` in `jobs/ai-job-drafts.ts`, beside the band\n * arithmetic the create route and the draft writer share.\n */\n\n/** One creation kind, as a job on this site may make it or not. */\nexport interface AiPlanCreation {\n allowed: boolean\n /** How many more the site may hold; `null` when the plan counts none. */\n left: number | null\n /**\n * Why not, as a clause a sentence completes: \"this workspace's plan does\n * not include reusable components\". `null` when allowed.\n */\n reason: string | null\n}\n\nexport interface AiPlanCapabilities {\n /**\n * Whether the workspace keeps reusable components. Where it does not, a\n * list's repeated items are drawn in one section. Forms are not read off\n * it: whether one may be created is `create.form` (AGL-3596).\n */\n reusableComponents: boolean\n /** Every creation kind a plan may name, and whether this job may make one here. */\n create: Readonly<Record<AiBuildPlanCreateKind, AiPlanCreation>>\n /**\n * Whether the workspace spends the Free taste, whose monthly credits are a\n * wall (AGL-2925): a plan is then held to the sections the wall's worst\n * case pays for (AGL-3070). Absent is `false`.\n */\n freeTaste?: boolean\n /**\n * A Free workspace's site scaffold (AGL-3594): the most pages its plan may\n * build, which the plan step sets for a `site` job on the Free taste. The\n * Free wall then holds the plan to that many pages and to the sections a\n * SITE plan's worst case leaves room for. Absent for every other job.\n */\n freeSitePages?: number\n /**\n * Whether this job's pages are written in the compact layout language\n * (AGL-3660), whose compiler draws a section's repeated items itself: as\n * instances of a component the plan places there, or, with none, in their\n * compact form, a title over its text, which is smaller than a block rule 1\n * calls a repeat. A plan is then not asked for a component for a section's\n * repeated items (AGL-3616); a section two pages share still is one. The\n * plan step sets it for a `build` job. Absent is `false`.\n *\n * It is the plan half of one rule. The page half is the tree check's\n * `AiDoctrineTreeContext.repeatsCompiled`, which every page the layout\n * language compiles is checked with: where the site's design draws a\n * section's items as picture cards — larger than the compact form, and so\n * a block rule 1 would count — those cards are the compiler's, not a block\n * typed out twice, and rule 1 asks no component of them (AGL-3660).\n */\n repeatsCompiled?: boolean\n /**\n * The most datasets this job's plan keeps (AGL-3616): a site start builds\n * at most a few. A plan naming more is settled in code before the rules\n * read it (`aiSettlePlanDatasets`), never re-asked. Absent is no cap beyond\n * `create.dataset.left`.\n */\n datasetsMax?: number\n /**\n * The fewest pieces a section that shows the work is planned with\n * (AGL-3754): a site start's `AI_SITE_GALLERY_MIN_ITEMS`. A plan naming\n * fewer is raised to it in code before the rules read it\n * (`aiSettlePlanGalleries`), never re-asked. Absent leaves every count as\n * planned.\n */\n galleryMinItems?: number\n}\n\n/** What a job of one kind builds from its own plan, where it builds only some creations. */\nexport interface AiPlanJobScope {\n /** The job as a refusal names it: \"a page job\". */\n noun: string\n /** The creation kinds the job builds itself; any other is refused before the plan is kept. */\n creates: readonly AiBuildPlanCreateKind[]\n}\n\nconst ALLOWED: AiPlanCreation = { allowed: true, left: null, reason: null }\n\n/**\n * Capabilities that restrict nothing: what a plan is held to when no\n * workspace was read — an org-level job, an eval, a spec about something\n * else. The doctrine applies whole.\n */\nexport function aiUnrestrictedPlanCapabilities(): AiPlanCapabilities {\n return {\n reusableComponents: true,\n create: Object.fromEntries(\n AI_BUILD_PLAN_CREATE_KINDS.map((kind) => [kind, ALLOWED]),\n ) as Record<AiBuildPlanCreateKind, AiPlanCreation>,\n }\n}\n\n/**\n * The workspace's capabilities narrowed to what a job of one kind builds.\n * A creation the workspace itself cannot make keeps the workspace's reason,\n * which is the one a member can act on; one the job does not build says so.\n */\nexport function aiPlanCapabilitiesForJob(\n capabilities: AiPlanCapabilities,\n scope: AiPlanJobScope | null | undefined,\n): AiPlanCapabilities {\n if (!scope) return capabilities\n const create = { ...capabilities.create }\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n if (!create[kind].allowed || scope.creates.includes(kind)) continue\n create[kind] = { allowed: false, left: null, reason: `${scope.noun} does not build one` }\n }\n return { ...capabilities, create }\n}\n\n/** \"a layout\", \"an email design\": a creation kind with its article. */\nexport function aiCreationNoun(kind: AiBuildPlanCreateKind): string {\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n return `${/^[aeiou]/.test(noun) ? 'an' : 'a'} ${noun}`\n}\n\n/**\n * The sentence a request states when the workspace keeps no reusable\n * components. It says a list's repeated items go in one section (AGL-3071):\n * \"a repeated item in its own section\" reads just as well as a section for\n * each item, which is how a live About page's four practice areas came to be\n * planned as four sections of one item.\n */\nexport const AI_PLAN_INLINE_SENTENCE =\n \"This workspace keeps no reusable components: draw a list's repeated items in one section.\"\n\n/**\n * The sentence a build's request states (AGL-3616): its pages are written in\n * the layout language, which draws a section's repeated items itself, so the\n * plan counts them in the section's `items` and needs no component for them.\n */\nexport const AI_PLAN_COMPILED_REPEATS_SENTENCE =\n \"A section's repeated items need no component here: count them in its items and the page draws them. A section two pages share is still one component.\"\n\n/**\n * The sentence a request states when the site has no saved form and this job\n * may create none (AGL-3596): the plan places no form, rather than one drawn\n * on the page with no saved form behind it.\n */\nexport const AI_PLAN_NO_FORM_SENTENCE =\n 'This site has no saved form and may create none: plan no form, and no section that collects answers.'\n\n/**\n * What the plan step's user turn says the job may create here: one line a\n * kind, and the inline sentence where the workspace keeps no reusable\n * components. Per workspace and per site, so it is never a system block.\n */\nexport function aiPlanCapabilityLines(capabilities: AiPlanCapabilities): string[] {\n const lines = ['What this job may create on this site:']\n for (const kind of AI_BUILD_PLAN_CREATE_KINDS) {\n const entry = capabilities.create[kind]\n const noun = AI_BUILD_PLAN_CREATION_NOUNS[kind].noun\n if (entry.allowed) {\n lines.push(`- ${noun}: yes${entry.left === null ? '' : `, ${entry.left} more`}`)\n } else {\n lines.push(`- ${noun}: no, because ${entry.reason ?? 'it cannot be made here'}`)\n }\n }\n if (!capabilities.reusableComponents) lines.push(AI_PLAN_INLINE_SENTENCE)\n return lines\n}\n\n/**\n * Whether a plan on this site can place a saved form at all (AGL-3596): the\n * site has one, or the job may create one. Absent capabilities restrict\n * nothing, as everywhere else.\n */\nexport function aiPlanCanPlaceForm(\n inventory: { forms: readonly unknown[] } | null,\n capabilities: Pick<AiPlanCapabilities, 'create'> | null,\n): boolean {\n if (inventory?.forms.length) return true\n return capabilities?.create.form.allowed !== false\n}\n\n/** What to do instead of a creation the job may not make, for the re-ask and the review. */\nfunction insteadOf(kind: AiBuildPlanCreateKind, capabilities: AiPlanCapabilities): string {\n switch (kind) {\n case 'component':\n return capabilities.reusableComponents\n ? 'Place a component the site already has.'\n : \"Draw a list's repeated items in one section instead.\"\n case 'form':\n return 'Place a form the site already has, or leave the form off the page.'\n case 'layout':\n return 'Put the page in a layout the site already has.'\n case 'template':\n return 'Build the page without a template.'\n case 'theme-change':\n return \"Use the colors the theme already has.\"\n case 'dataset':\n return 'Bind a dataset or collection the site already has, or keep the list short enough to write out.'\n case 'email':\n return 'Leave the email out of this job.'\n }\n}\n\n/** One creation a plan names that this job may not make here. */\nexport interface AiPlanUncreatable {\n kind: AiBuildPlanCreateKind\n name: string\n /** The plan path of the creation: `create[2]`. */\n path: string\n /** Why not, as a clause: \"this site has room for 1 more\". */\n reason: string\n /** One customer-safe sentence: what was refused, why, and what to do instead. */\n message: string\n}\n\n/**\n * Why the nth creation of a kind may not be made here, as a clause; `null`\n * when it may. A kind the site may make only so many more of refuses the ones\n * past that count, not the first of them.\n */\nfunction refusedReason(capability: AiPlanCreation, nth: number): string | null {\n const overCount = capability.allowed && capability.left !== null && nth > capability.left\n if (capability.allowed && !overCount) return null\n return overCount\n ? `this site has room for ${capability.left} more`\n : (capability.reason ?? 'it cannot be made here')\n}\n\n/** The creations a plan names that the capabilities refuse, in plan order. */\nexport function aiPlanUncreatable(\n plan: Pick<AiBuildPlan, 'create'>,\n capabilities: AiPlanCapabilities,\n): AiPlanUncreatable[] {\n const refused: AiPlanUncreatable[] = []\n const counted = new Map<AiBuildPlanCreateKind, number>()\n plan.create.forEach((entry, index) => {\n const nth = (counted.get(entry.kind) ?? 0) + 1\n counted.set(entry.kind, nth)\n const reason = refusedReason(capabilities.create[entry.kind], nth)\n if (reason === null) return\n refused.push({\n kind: entry.kind,\n name: entry.name,\n path: `create[${index}]`,\n reason,\n message: `The plan creates ${aiCreationNoun(entry.kind)} named \"${entry.name}\", and ${reason}. ${insteadOf(entry.kind, capabilities)}`,\n })\n })\n return refused\n}\n\n/**\n * Why the plan could not add one more creation of a kind here, and what to\n * do instead; `null` when it could (AGL-3040). Counted past the creations of\n * that kind the plan already names, as `aiPlanUncreatable` counts them, so a\n * plan told to declare a creation is never told to declare one the next\n * answer is refused for.\n */\nexport function aiPlanUncreatableKind(\n plan: Pick<AiBuildPlan, 'create'>,\n kind: AiBuildPlanCreateKind,\n capabilities: AiPlanCapabilities,\n): { reason: string; instead: string } | null {\n const planned = plan.create.filter((entry) => entry.kind === kind).length\n const reason = refusedReason(capabilities.create[kind], planned + 1)\n return reason === null ? null : { reason, instead: insteadOf(kind, capabilities) }\n}\n"],"names":["AI_BUILD_PLAN_CREATE_KINDS","AI_BUILD_PLAN_CREATION_NOUNS","ALLOWED","allowed","left","reason","aiUnrestrictedPlanCapabilities","reusableComponents","create","Object","fromEntries","map","kind","aiPlanCapabilitiesForJob","capabilities","scope","creates","includes","noun","aiCreationNoun","test","AI_PLAN_INLINE_SENTENCE","AI_PLAN_COMPILED_REPEATS_SENTENCE","AI_PLAN_NO_FORM_SENTENCE","aiPlanCapabilityLines","lines","entry","push","aiPlanCanPlaceForm","inventory","forms","length","form","insteadOf","refusedReason","capability","nth","overCount","aiPlanUncreatable","plan","refused","counted","Map","forEach","index","get","set","name","path","message","aiPlanUncreatableKind","planned","filter","instead"],"mappings":"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"}
@@ -29,7 +29,8 @@ import type { AiJobOutput, AiJobSummary } from './ai-jobs.types';
29
29
  * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one
30
30
  * product a pass, each read from the catalog as it is reached.
31
31
  * - `catalog` — a store's first products from the brief: names, copy, tags
32
- * and options, and what each photo should show. No price and no picture.
32
+ * and options, what each photo should show, and a suggested price by what
33
+ * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.
33
34
  * - `categories` — store categories and a first set of discounts from the brief.
34
35
  *
35
36
  * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the
@@ -76,6 +77,29 @@ export declare const AI_CATALOG_OPTION_VALUES_MAX = 6;
76
77
  export declare const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30;
77
78
  /** What a proposed product's photo should show, in one sentence. */
78
79
  export declare const AI_CATALOG_PHOTO_MAX_CHARS = 160;
80
+ /**
81
+ * A proposed product's suggested price, in US dollars — the one currency a
82
+ * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,
83
+ * 2026-10-12): a store's first products priced by what each one is (a bar
84
+ * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead
85
+ * of every one at the commerce default. Never $0 or less, nothing past what
86
+ * a small shop's first product plausibly lists at; a price outside the range,
87
+ * or none, is no price, and the writer gives the product its default price.
88
+ * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.
89
+ */
90
+ export declare const AI_CATALOG_PRICE_USD: {
91
+ readonly min: 1;
92
+ readonly max: 5000;
93
+ };
94
+ /**
95
+ * A proposed price as code keeps it: a finite number of dollars (or a plain
96
+ * "$12.50" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to
97
+ * whole cents; anything else — missing, zero, negative, absurd, not a number
98
+ * — is `null`, so the product takes the store's default price for the owner
99
+ * to change. The model is never re-asked for a price: a bad one costs the
100
+ * product only its suggestion.
101
+ */
102
+ export declare function aiCatalogPrice(raw: unknown): number | null;
79
103
  export declare const AI_CATEGORIES_MAX = 12;
80
104
  export declare const AI_DISCOUNTS_MAX = 5;
81
105
  export declare const AI_CATEGORY_NAME_MAX_CHARS = 60;
@@ -197,6 +221,12 @@ export interface AiCatalogProduct {
197
221
  seoDescription: string;
198
222
  /** What the product's photo should show; nothing is uploaded or linked. */
199
223
  photo: string;
224
+ /**
225
+ * The suggested price for every variant, in US dollars, as `aiCatalogPrice`
226
+ * kept it; `null` (or absent, on a proposal made before 2026-10-12) when
227
+ * the model gave none in range, and the store's default price stands in.
228
+ */
229
+ price?: number | null;
200
230
  }
201
231
  export interface AiCatalogProposal {
202
232
  kind: 'catalog';
@@ -27,7 +27,8 @@
27
27
  * - `bulk` — the copy of up to `AI_PRODUCTS_BULK_MAX` saved products, one
28
28
  * product a pass, each read from the catalog as it is reached.
29
29
  * - `catalog` — a store's first products from the brief: names, copy, tags
30
- * and options, and what each photo should show. No price and no picture.
30
+ * and options, what each photo should show, and a suggested price by what
31
+ * the product is (`aiCatalogPrice`, Zach 2026-10-12). No picture.
31
32
  * - `categories` — store categories and a first set of discounts from the brief.
32
33
  *
33
34
  * Every output is a PROPOSAL (`resource: 'product'`, with `proposal`), and the
@@ -69,6 +70,38 @@ export const AI_CATALOG_TAGS_MAX = 6;
69
70
  export const AI_CATALOG_OPTION_VALUES_MAX = 6;
70
71
  export const AI_CATALOG_OPTION_VALUE_MAX_CHARS = 30;
71
72
  /** What a proposed product's photo should show, in one sentence. */ export const AI_CATALOG_PHOTO_MAX_CHARS = 160;
73
+ /**
74
+ * A proposed product's suggested price, in US dollars — the one currency a
75
+ * catalog prices in (`priceUsd`) — and the range code keeps it to (Zach,
76
+ * 2026-10-12): a store's first products priced by what each one is (a bar
77
+ * of handmade soap about $8–12, a candle $18–32, a gift set $30–45) instead
78
+ * of every one at the commerce default. Never $0 or less, nothing past what
79
+ * a small shop's first product plausibly lists at; a price outside the range,
80
+ * or none, is no price, and the writer gives the product its default price.
81
+ * Below the commerce ceiling (`COMMERCE_MAX_PRICE_USD`, $10,000) by design.
82
+ */ export const AI_CATALOG_PRICE_USD = {
83
+ min: 1,
84
+ max: 5000
85
+ };
86
+ /**
87
+ * A proposed price as code keeps it: a finite number of dollars (or a plain
88
+ * "$12.50" a model wrote as text) inside `AI_CATALOG_PRICE_USD`, rounded to
89
+ * whole cents; anything else — missing, zero, negative, absurd, not a number
90
+ * — is `null`, so the product takes the store's default price for the owner
91
+ * to change. The model is never re-asked for a price: a bad one costs the
92
+ * product only its suggestion.
93
+ */ export function aiCatalogPrice(raw) {
94
+ let value = null;
95
+ if (typeof raw === 'number') value = raw;
96
+ else if (typeof raw === 'string') {
97
+ const match = /^\s*(?:US\s*)?\$?\s*(\d{1,6}(?:\.\d{1,2})?)\s*(?:USD)?\s*$/i.exec(raw);
98
+ value = match ? Number(match[1]) : null;
99
+ }
100
+ if (value === null || !Number.isFinite(value)) return null;
101
+ const cents = Math.round(value * 100);
102
+ if (cents < AI_CATALOG_PRICE_USD.min * 100 || cents > AI_CATALOG_PRICE_USD.max * 100) return null;
103
+ return cents / 100;
104
+ }
72
105
  export const AI_CATEGORIES_MAX = 12;
73
106
  export const AI_DISCOUNTS_MAX = 5;
74
107
  export const AI_CATEGORY_NAME_MAX_CHARS = 60;
@@ -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'\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, and what each photo should show. No price and 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/** The most products one bulk job writes the copy of. */\nexport const AI_PRODUCTS_BULK_MAX = 50\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\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 {\n target: 'bulk',\n productIds: [...new Set(input.productIds.filter((id) => ID.test(id)))]\n .slice(0, AI_PRODUCTS_BULK_MAX)\n .join(','),\n }\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\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\n/** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */\nexport const aiProductsBulkBrief = (count: number): string =>\n `Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`\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_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_PRODUCTS_BULK_MAX","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_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","value","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","Math","productIds","Set","AI_PRODUCTS_NO_TARGET_COPY","AI_PRODUCTS_NO_NAME_COPY","AI_PRODUCTS_NO_SELECTION_COPY","AI_PRODUCTS_TOO_MANY_COPY","parseAiProductsJobInputs","raw","includes","String","split","part","ids","unique","AI_PRODUCTS_OUTPUT_IDS","copy","catalog","categories","isRecord","aiProductsProposalOf","output","resource","proposal","kind","aiProductCopyProposals","job","outputs","aiProductsProposalOfKind","aiProductGaps","texts","found","match","matchAll","add","AI_PRODUCTS_IMPORT_WRITE_COPY","aiProductsBulkBrief","count","BULK_BRIEF","aiProductsBulkCount","exec","brief","Number"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,OAAO,MAAMA,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,uDAAuD,GACvD,OAAO,MAAMC,uBAAuB,GAAE;AAEtC,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,OAAO,MAAMC,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,CAACI,OAAgBrB,MAC5B,OAAOqB,UAAU,WAAWA,MAAMC,OAAO,CAAC,QAAQ,KAAKC,IAAI,GAAGC,KAAK,CAAC,GAAGxB,OAAO;AAEhF,6EAA6E,GAC7E,SAASyB,YAAYJ,KAAc,EAAErB,GAAW,EAAE0B,QAAgB;IAChE,IAAI,OAAOL,UAAU,YAAY,CAACA,MAAME,IAAI,IAAI,OAAO,EAAE;IACzD,IAAI;QACF,MAAMI,SAAkBC,KAAKC,KAAK,CAACR;QACnC,IAAI,CAACS,MAAMC,OAAO,CAACJ,SAAS,OAAO,EAAE;QACrC,OAAOA,OACJK,GAAG,CAAC,CAACC,QAAUhB,KAAKgB,OAAOP,WAC3BQ,MAAM,CAACC,SACPX,KAAK,CAAC,GAAGxB;IACd,EAAE,eAAM;QACN,OAAO,EAAE;IACX;AACF;AAEA,SAASoC,YAAYf,KAAc;IACjC,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAME,IAAI,IAAI,OAAO,EAAE;IACzD,IAAI;QACF,MAAMI,SAAkBC,KAAKC,KAAK,CAACR;QACnC,IAAI,CAACS,MAAMC,OAAO,CAACJ,SAAS,OAAO,EAAE;QACrC,OAAOA,OACJK,GAAG,CAAC,CAACC;YACJ,MAAMI,SAAUJ,gBAAAA,QAAS,CAAC;YAC1B,OAAO;gBACLjB,MAAMC,KAAKoB,MAAM,CAAC,OAAO,EAAEzC;gBAC3B0C,QAAQR,MAAMC,OAAO,CAACM,MAAM,CAAC,SAAS,IAClCA,MAAM,CAAC,SAAS,CAACL,GAAG,CAAC,CAACX,QAAUJ,KAAKI,OAAO,KAAKa,MAAM,CAACC,SAASX,KAAK,CAAC,GAAG7B,gCAC1E,EAAE;YACR;QACF,GACC6B,KAAK,CAAC,GAAG9B;IACd,EAAE,eAAM;QACN,OAAO,EAAE;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,MAAM6C,oCAAoC,KAAK;AAEtD;;;;;CAKC,GACD,OAAO,SAASC,oBAAoBC,KAA0B;IAC5D,OAAQA,MAAMC,MAAM;QAClB,KAAK;YAAW;oBAID3B,aAYAA;gBAfb,MAAMA,UAAU0B,MAAM1B,OAAO;gBAC7B,MAAM4B,SAAiC;oBACrCD,QAAQ;oBACRE,SAAS,GAAE7B,cAAAA,QAAQ8B,EAAE,YAAV9B,cAAc;oBACzBC,MAAMC,KAAKF,QAAQC,IAAI,EAAE7B;oBACzB2D,MAAM/B,QAAQ+B,IAAI;oBAClB7B,MAAMF,QAAQE,IAAI,CAACO,KAAK,CAAC,GAAGpC;oBAC5B8B,MAAMU,KAAKmB,SAAS,CAAChC,QAAQG,IAAI,CAACM,KAAK,CAAC,GAAG,IAAIQ,GAAG,CAAC,CAACgB,MAAQA,IAAIxB,KAAK,CAAC,GAAG;oBACzEyB,aAAalC,QAAQkC,WAAW,CAACf,MAAM,CAAC,CAACW,KAAOzB,GAAG8B,IAAI,CAACL,KAAKrB,KAAK,CAAC,GAAG,IAAIL,IAAI,CAAC;oBAC/EgC,SAASvB,KAAKmB,SAAS,CACrBhC,QAAQoC,OAAO,CAAC3B,KAAK,CAAC,GAAG9B,wBAAwBsC,GAAG,CAAC,CAACoB,SAAY,CAAA;4BAChEpC,MAAMoC,OAAOpC,IAAI,CAACQ,KAAK,CAAC,GAAG5B;4BAC3B0C,QAAQc,OAAOd,MAAM,CAACd,KAAK,CAAC,GAAG7B,8BAA8BqC,GAAG,CAAC,CAACX,QAAUA,MAAMG,KAAK,CAAC,GAAG;wBAC7F,CAAA;oBAEF6B,UAAU,EAACtC,oBAAAA,QAAQsC,QAAQ,YAAhBtC,oBAAoB,IAAIS,KAAK,CAAC,GAAG;oBAC5C8B,UAAUvC,QAAQuC,QAAQ,CAAC9B,KAAK,CAAC,GAAG;oBACpC+B,gBAAgBxC,QAAQwC,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,GAAGiC,KAAKzD,GAAG,CAAC,GAAG2C,MAAM,CAAC,OAAO,CAACa,MAAM,GAAG;gBAC/E;gBACA,OAAOb;YACT;QACA,KAAK;YACH,OAAO;gBACLD,QAAQ;gBACRgB,YAAY;uBAAI,IAAIC,IAAIlB,MAAMiB,UAAU,CAACxB,MAAM,CAAC,CAACW,KAAOzB,GAAG8B,IAAI,CAACL;iBAAM,CACnErB,KAAK,CAAC,GAAG3B,sBACTsB,IAAI,CAAC;YACV;QACF;YACE,OAAO;gBAAEuB,QAAQD,MAAMC,MAAM;YAAC;IAClC;AACF;AAEA,4EAA4E,GAC5E,OAAO,MAAMkB,6BAA6B,4DAA2D;AACrG,OAAO,MAAMC,2BAA2B,mDAAkD;AAC1F,OAAO,MAAMC,gCAAgC,uCAAsC;AACnF,OAAO,MAAMC,4BAA4B,CAAC,aAAa,EAAElE,qBAAqB,oBAAoB,CAAC,CAAA;AAEnG,6EAA6E,GAC7E,OAAO,SAASmE,yBAAyBC,GAAkD;IACzF,MAAMtB,SAASsB,cAAAA,MAAO,CAAC;IACvB,MAAMvB,SAASC,MAAM,CAAC,SAAS;IAC/B,IAAI,CAAC,AAAC1D,oBAA2CiF,QAAQ,CAACxB,SAAS,OAAOkB;IAC1E,IAAIlB,WAAW,WAAW;YAaAC;QAZxB,MAAM3B,OAAOC,KAAK0B,MAAM,CAAC,OAAO,EAAExD;QAClC,IAAI,CAAC6B,MAAM,OAAO6C;QAClB,MAAMf,OAAOH,MAAM,CAAC,OAAO;QAC3B,MAAME,KAAK,OAAOF,MAAM,CAAC,YAAY,KAAK,YAAYvB,GAAG8B,IAAI,CAACP,MAAM,CAAC,YAAY,IAAIA,MAAM,CAAC,YAAY,GAAG;QAC3G,OAAO;YACLD;YACA3B,SAAS;gBACP8B;gBACA7B;gBACA8B,MAAM,AAAC5D,iBAAwCgF,QAAQ,CAACpB,QAASA,OAAyB;gBAC1F7B,MAAM,OAAO0B,MAAM,CAAC,OAAO,KAAK,WAAWA,MAAM,CAAC,OAAO,CAACpB,IAAI,GAAGC,KAAK,CAAC,GAAGpC,6BAA6B;gBACvG8B,MAAMO,YAAYkB,MAAM,CAAC,OAAO,EAAE,IAAI;gBACtCM,aAAakB,QAAOxB,sBAAAA,MAAM,CAAC,cAAc,YAArBA,sBAAyB,IAC1CyB,KAAK,CAAC,KACNpC,GAAG,CAAC,CAACqC,OAASA,KAAK9C,IAAI,IACvBW,MAAM,CAAC,CAACmC,OAASjD,GAAG8B,IAAI,CAACmB,OACzB7C,KAAK,CAAC,GAAG;gBACZ2B,SAASf,YAAYO,MAAM,CAAC,UAAU;gBACtCU,UAAUpC,KAAK0B,MAAM,CAAC,WAAW,EAAE,QAAQ;gBAC3CW,UAAUrC,KAAK0B,MAAM,CAAC,WAAW,EAAE;gBACnCY,gBAAgBtC,KAAK0B,MAAM,CAAC,iBAAiB,EAAE;YACjD;QACF;IACF;IACA,IAAID,WAAW,QAAQ;YACFC;QAAnB,MAAM2B,MAAMH,QAAOxB,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB,IACxCyB,KAAK,CAAC,KACNpC,GAAG,CAAC,CAACqC,OAASA,KAAK9C,IAAI,IACvBW,MAAM,CAAC,CAACmC,OAASjD,GAAG8B,IAAI,CAACmB;QAC5B,MAAME,SAAS;eAAI,IAAIZ,IAAIW;SAAK;QAChC,IAAI,CAACC,OAAOf,MAAM,EAAE,OAAOM;QAC3B,IAAIS,OAAOf,MAAM,GAAG3D,sBAAsB,OAAOkE;QACjD,OAAO;YAAErB;YAAQgB,YAAYa;QAAO;IACtC;IACA,OAAO;QAAE7B,QAAQA;IAAmC;AACtD;AA0FA,0EAA0E,GAC1E,OAAO,MAAM8B,yBAAyB;IACpCC,MAAM,CAAC7B,YAA6B,CAAC,KAAK,EAAEA,oBAAAA,YAAa,OAAO;IAChE8B,SAAS;IACTC,YAAY;AACd,EAAU;AAEV,MAAMC,WAAW,CAACvD,QAChB,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACS,MAAMC,OAAO,CAACV;AAEhE,mFAAmF,GACnF,OAAO,SAASwD,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,EACvBpD,GAAG,CAAC,CAAC8C,SAAWD,qBAAqBC,SACrC5C,MAAM,CAAC,CAAC8C,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,IAAI7B;IAClB,KAAK,MAAMtC,SAASkE,MAAO;QACzB,KAAK,MAAME,SAASpE,MAAMqE,QAAQ,CAAC,wBAAyBF,MAAMG,GAAG,CAACF,KAAK,CAAC,EAAE,CAAClE,IAAI;IACrF;IACA,OAAO;WAAIiE;KAAM,CAACtD,MAAM,CAACC;AAC3B;AAEA;;4EAE4E,GAE5E,0EAA0E,GAC1E,OAAO,MAAMyD,gCAAgC,eAAc;AAE3D,8FAA8F,GAC9F,OAAO,MAAMC,sBAAsB,CAACC,QAClC,CAAC,0BAA0B,EAAEA,MAAM,CAAC,EAAEA,UAAU,IAAI,YAAY,YAAY,CAAA;AAE9E,MAAMC,aAAa;AAEnB,oGAAoG,GACpG,OAAO,SAASC,oBAAoBb,GAAmD;;IACrF,MAAMM,QAAQM,WAAWE,IAAI,SAACd,uBAAAA,IAAKe,KAAK,mBAAI;IAC5C,OAAOT,QAAQU,OAAOV,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'\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/** The most products one bulk job writes the copy of. */\nexport const AI_PRODUCTS_BULK_MAX = 50\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 {\n target: 'bulk',\n productIds: [...new Set(input.productIds.filter((id) => ID.test(id)))]\n .slice(0, AI_PRODUCTS_BULK_MAX)\n .join(','),\n }\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\n/** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */\nexport const aiProductsBulkBrief = (count: number): string =>\n `Write storefront copy for ${count} ${count === 1 ? 'product' : 'products'}`\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_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_PRODUCTS_BULK_MAX","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","Set","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","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","aiProductsBulkBrief","count","BULK_BRIEF","aiProductsBulkCount","brief"],"mappings":"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"}
@@ -105,6 +105,21 @@ export interface AiInventoryDataset {
105
105
  * (AGL-3616): what a record template's Image binds as its `src`.
106
106
  */
107
107
  imageField?: string;
108
+ /**
109
+ * The id of the record's page-address field, where it has one (AGL-3727):
110
+ * what a listing's card links each record's own page by. Where the site
111
+ * already serves record pages for the dataset, the field those pages are
112
+ * served by (AGL-3747).
113
+ */
114
+ addressField?: string;
115
+ /**
116
+ * Where this site already serves each record a page of its own (AGL-3747):
117
+ * the base of the dataset's saved record template (AGL-3475), without
118
+ * slashes at either end — `team` for `/team/ada`. Absent where the site
119
+ * keeps no record template for it. Never shown in the prompt: code links a
120
+ * listing's cards by it.
121
+ */
122
+ recordBase?: string;
108
123
  }
109
124
  export interface AiInventoryCollection {
110
125
  id: string;
@@ -176,6 +191,8 @@ export declare function aiInventoryHaystack(kind: AiInventoryKind, row: AiInvent
176
191
  * with nowhere to go is sent there, which is why the doctrine names them.
177
192
  */
178
193
  export declare function aiHomeScreenIds(inventory: AiSiteInventory | null): string[];
194
+ /** The titles of the site's home screens ({@link aiHomeScreenIds}): a link under one says home, in the site's own language (AGL-3760). */
195
+ export declare function aiHomeScreenNames(inventory: AiSiteInventory | null): string[];
179
196
  /** A site the org does not own; the inventory is never read for it. */
180
197
  export declare class AiInventoryScopeError extends Error {
181
198
  constructor(hostId: string);
@@ -126,6 +126,11 @@ export const AI_INVENTORY_KINDS = [
126
126
  const kept = homes.filter((screen)=>!screen.replaceable);
127
127
  return (kept.length ? kept : homes).map((screen)=>screen.id);
128
128
  }
129
+ /** The titles of the site's home screens ({@link aiHomeScreenIds}): a link under one says home, in the site's own language (AGL-3760). */ export function aiHomeScreenNames(inventory) {
130
+ var _ref;
131
+ const ids = new Set(aiHomeScreenIds(inventory));
132
+ return ((_ref = inventory == null ? void 0 : inventory.screens) != null ? _ref : []).filter((screen)=>ids.has(screen.id) && screen.name.trim()).map((screen)=>screen.name.trim());
133
+ }
129
134
  /** A site the org does not own; the inventory is never read for it. */ export class AiInventoryScopeError extends Error {
130
135
  constructor(hostId){
131
136
  super(`host ${hostId} is not a site of the workspace that asked`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n /**\n * The id of the field holding each record's photo, where it has one\n * (AGL-3616): what a record template's Image binds as its `src`.\n */\n imageField?: string\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n /**\n * The platform's starter home page, untouched since the site was created\n * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering\n * `/`, with no version but the one it was provisioned with. A plan may put\n * its own home page at its address, and publishing that page retires this\n * one (AGL-3408). Absent on every other screen, and on a starter home the\n * owner has edited, which a plan keeps and plans around.\n */\n replaceable?: true\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}${\n screen.replaceable ? ' \\u00b7 starter home, a planned home page at / replaces it' : ''\n }`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n const homes = (inventory?.screens ?? []).filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n // A starter home a planned home page replaces (AGL-3594) is retired when\n // that page is published, so a link is sent to the page that replaces it\n // wherever the site already has one.\n const kept = homes.filter((screen) => !screen.replaceable)\n return (kept.length ? kept : homes).map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","replaceable","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","homes","filter","has","trim","replace","test","kept","length","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n /**\n * The id of the field holding each record's photo, where it has one\n * (AGL-3616): what a record template's Image binds as its `src`.\n */\n imageField?: string\n /**\n * The id of the record's page-address field, where it has one (AGL-3727):\n * what a listing's card links each record's own page by. Where the site\n * already serves record pages for the dataset, the field those pages are\n * served by (AGL-3747).\n */\n addressField?: string\n /**\n * Where this site already serves each record a page of its own (AGL-3747):\n * the base of the dataset's saved record template (AGL-3475), without\n * slashes at either end — `team` for `/team/ada`. Absent where the site\n * keeps no record template for it. Never shown in the prompt: code links a\n * listing's cards by it.\n */\n recordBase?: string\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n /**\n * The platform's starter home page, untouched since the site was created\n * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering\n * `/`, with no version but the one it was provisioned with. A plan may put\n * its own home page at its address, and publishing that page retires this\n * one (AGL-3408). Absent on every other screen, and on a starter home the\n * owner has edited, which a plan keeps and plans around.\n */\n replaceable?: true\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}${\n screen.replaceable ? ' \\u00b7 starter home, a planned home page at / replaces it' : ''\n }`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n const homes = (inventory?.screens ?? []).filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n // A starter home a planned home page replaces (AGL-3594) is retired when\n // that page is published, so a link is sent to the page that replaces it\n // wherever the site already has one.\n const kept = homes.filter((screen) => !screen.replaceable)\n return (kept.length ? kept : homes).map((screen) => screen.id)\n}\n\n/** The titles of the site's home screens ({@link aiHomeScreenIds}): a link under one says home, in the site's own language (AGL-3760). */\nexport function aiHomeScreenNames(inventory: AiSiteInventory | null): string[] {\n const ids = new Set(aiHomeScreenIds(inventory))\n return (inventory?.screens ?? []).filter((screen) => ids.has(screen.id) && screen.name.trim()).map((screen) => screen.name.trim())\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","replaceable","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","homes","filter","has","trim","replace","test","kept","length","aiHomeScreenNames","ids","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
@@ -154,7 +154,7 @@ export declare function aiFreeSiteSectionsWithin(creations: AiFreeSiteCreations,
154
154
  * (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what the
155
155
  * plan step allows) and the room for one retried page.
156
156
  *
157
- * The CEILING a start is quoted with ("up to 282"), no longer the figure it
157
+ * The CEILING a start is quoted with ("up to 283"), no longer the figure it
158
158
  * is admitted on (AGL-3722): that is the measured p90
159
159
  * (`aiFreeSiteCreditRange`), and a start past what is left is asked to be
160
160
  * confirmed and pauses with Resume when its credits run out, instead of
@@ -197,7 +197,7 @@ export declare function aiFreeCreditsResetLabel(resetsOn: string): string;
197
197
  * nominal section count, at what each MEASURED in production
198
198
  * (`AI_MEASURED_UNIT_CREDITS`); the ceiling is the whole worst case the wall
199
199
  * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 137
200
- * (p90 213, up to 282).
200
+ * (p90 213, up to 283).
201
201
  */
202
202
  export declare function aiFreeSiteCreditRange(pages: number): AiCreditRange;
203
203
  /** The smaller first build a site start offers: its home page alone. */
@@ -597,7 +597,16 @@ export declare function aiJobPlanCreditEstimate(kind: string, plan: AiBuildPlan)
597
597
  */
598
598
  export declare function aiSiteCreditEstimate(pages: number, options?: {
599
599
  welcomeEmail?: boolean;
600
+ siteKind?: string | null;
600
601
  }): number;
602
+ /** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`). */
603
+ export declare const AI_SITE_START_POSTS = 3;
604
+ /**
605
+ * The first content a paid start of this kind writes (AGL-3676): a store's
606
+ * first products, a blog's first posts, or nothing. `aiSiteContentPart`'s
607
+ * answer for a paid start, by the kind's id.
608
+ */
609
+ export declare function aiSiteStartContent(siteKind: string | null | undefined): 'posts' | 'products' | null;
601
610
  /**
602
611
  * The measured cost of one creation a plan builds (AGL-3722): a layout's or
603
612
  * a form's as measured, a theme's as the site start's look measured, and
@@ -636,4 +645,5 @@ export declare function aiJobPlanCreditRange(kind: string, plan: AiBuildPlan): A
636
645
  */
637
646
  export declare function aiSiteCreditRange(pages: number, options?: {
638
647
  welcomeEmail?: boolean;
648
+ siteKind?: string | null;
639
649
  }): AiCreditRange;
@@ -19,7 +19,7 @@ import { AI_BUILD_PLAN_CREATION_NOUNS, aiPlanFormDatasetNames, aiPlanUndeclaredR
19
19
  import { AI_JOB_CREATE_KINDS } from "./ai-job-creations.js";
20
20
  import { AI_PAGE_CREATE_KINDS } from "./ai-page-job.js";
21
21
  import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
22
- import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, AI_MEASURED_STANDALONE_CREATION_CREDITS, AI_MEASURED_UNIT_CREDITS, aiCreditRangeAdd, aiCreditRangeOf, aiCreditRangeOrdered, aiCreditsPromptFor, aiMeasuredPageCredits, aiMeasuredStandalonePageCredits } from "./ai-credit-estimate.js";
22
+ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PAID_SITE_CREDITS, AI_MEASURED_PASS_CREDITS, AI_MEASURED_STANDALONE_CREATION_CREDITS, AI_MEASURED_UNIT_CREDITS, aiCreditRangeAdd, aiCreditRangeOf, aiCreditRangeOrdered, aiCreditsPromptFor, aiMeasuredPageCredits, aiMeasuredStandalonePageCredits } from "./ai-credit-estimate.js";
23
23
  /**
24
24
  * What a site scaffold is (AGL-2911): the inputs a `site` job is admitted
25
25
  * with, the plans a scaffold can build, and what one is estimated to cost
@@ -85,7 +85,7 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, AI_MEASURED_STANDALONE_
85
85
  * stand, priced as each field says. The spec fails when a figure it derives
86
86
  * moves and this does not.
87
87
  */ export const AI_FREE_SITE_WORST_CASE_CREDITS = {
88
- plan: 35,
88
+ plan: 36,
89
89
  look: 7,
90
90
  layout: 23,
91
91
  form: 29,
@@ -126,7 +126,7 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, AI_MEASURED_STANDALONE_
126
126
  * (`aiFreeSiteSectionsWithin` at the month's whole taste, which is what the
127
127
  * plan step allows) and the room for one retried page.
128
128
  *
129
- * The CEILING a start is quoted with ("up to 282"), no longer the figure it
129
+ * The CEILING a start is quoted with ("up to 283"), no longer the figure it
130
130
  * is admitted on (AGL-3722): that is the measured p90
131
131
  * (`aiFreeSiteCreditRange`), and a start past what is left is asked to be
132
132
  * confirmed and pauses with Resume when its credits run out, instead of
@@ -159,7 +159,7 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PASS_CREDITS, AI_MEASURED_STANDALONE_
159
159
  * nominal section count, at what each MEASURED in production
160
160
  * (`AI_MEASURED_UNIT_CREDITS`); the ceiling is the whole worst case the wall
161
161
  * is proven with (`aiFreeSiteCreditEstimate`). Two pages: about 137
162
- * (p90 213, up to 282).
162
+ * (p90 213, up to 283).
163
163
  */ export function aiFreeSiteCreditRange(pages) {
164
164
  const count = Math.max(1, Math.floor(pages));
165
165
  return aiCreditRangeOrdered(_extends({}, aiSiteStartMeasured(count), {
@@ -699,9 +699,20 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
699
699
  * step that proposes it. Shown where a member asks for a scaffold.
700
700
  */ export function aiSiteCreditEstimate(pages, options = {}) {
701
701
  const screens = Math.max(0, Math.floor(pages));
702
- const passes = 1 + screens * (AI_SITE_NOMINAL_SECTIONS + 1) + 2 + (options.welcomeEmail ? 1 : 0);
702
+ const content = aiSiteStartContent(options.siteKind);
703
+ const passes = 1 + screens * (AI_SITE_NOMINAL_SECTIONS + 1) + 2 + (options.welcomeEmail ? 1 : 0) + (content === 'products' ? 1 : content === 'posts' ? AI_SITE_START_POSTS : 0);
703
704
  return passes * AI_SITE_PASS_CREDITS;
704
705
  }
706
+ /** How many first posts a paid blog start writes, one pass each (`AI_SITE_POSTS`, held equal by `ai-site-job.spec.ts`). */ export const AI_SITE_START_POSTS = 3;
707
+ /**
708
+ * The first content a paid start of this kind writes (AGL-3676): a store's
709
+ * first products, a blog's first posts, or nothing. `aiSiteContentPart`'s
710
+ * answer for a paid start, by the kind's id.
711
+ */ export function aiSiteStartContent(siteKind) {
712
+ if (siteKind === 'store') return 'products';
713
+ if (siteKind === 'blog') return 'posts';
714
+ return null;
715
+ }
705
716
  /**
706
717
  * The measured cost of one creation a plan builds (AGL-3722): a layout's or
707
718
  * a form's as measured, a theme's as the site start's look measured, and
@@ -764,14 +775,30 @@ const ID_CHARS = /^[A-Za-z0-9_-]{1,64}$/;
764
775
  * ceiling is `aiSiteCreditEstimate`.
765
776
  */ export function aiSiteCreditRange(pages, options = {}) {
766
777
  const count = Math.max(0, Math.floor(pages));
767
- const measured = aiSiteStartMeasured(count);
768
- const email = options.welcomeEmail ? AI_MEASURED_PASS_CREDITS : {
769
- median: 0,
770
- p90: 0
771
- };
778
+ // A paid start plans and writes on the paid routing, measured apart (AGL-3739).
779
+ const { plan, theme, layout, form, page, email, products } = AI_MEASURED_PAID_SITE_CREDITS;
780
+ const content = aiSiteStartContent(options.siteKind);
781
+ // A blog's posts have no measurement of their own yet: the stand-in pass.
782
+ const parts = [
783
+ plan,
784
+ theme,
785
+ layout,
786
+ form,
787
+ ...Array.from({
788
+ length: count
789
+ }, ()=>page),
790
+ ...options.welcomeEmail ? [
791
+ email
792
+ ] : [],
793
+ ...content === 'products' ? [
794
+ products
795
+ ] : content === 'posts' ? [
796
+ AI_MEASURED_PASS_CREDITS
797
+ ] : []
798
+ ];
772
799
  return aiCreditRangeOrdered({
773
- likely: measured.likely + email.median,
774
- p90: measured.p90 + email.p90,
800
+ likely: parts.reduce((total, part)=>total + part.median, 0),
801
+ p90: parts.reduce((total, part)=>total + part.p90, 0),
775
802
  ceiling: aiSiteCreditEstimate(count, options)
776
803
  });
777
804
  }