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

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 (116) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +3 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +7 -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.js +6 -4
  13. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  14. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/jobs/ai-build-capabilities.js +162 -2
  17. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  18. package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
  19. package/src/lib/jobs/ai-job-build-step.js +85 -16
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  22. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  23. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  24. package/src/lib/jobs/ai-job-layout-language.js +13 -0
  25. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  26. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  27. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  29. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  30. package/src/lib/jobs/ai-job-page-language.js +21 -5
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-page-sections.js +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  35. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  38. package/src/lib/jobs/ai-job-products-step.js +2 -2
  39. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  41. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  42. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  43. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  44. package/src/lib/jobs/ai-job-site-content.js +28 -12
  45. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  46. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  47. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  48. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  49. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  50. package/src/lib/jobs/ai-job-site-step.js +70 -10
  51. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  53. package/src/lib/jobs/ai-site-publish.js +203 -56
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  56. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  57. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  58. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  59. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  60. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  61. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  62. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  63. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  68. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  69. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  70. package/src/lib/model/ai-build-job.d.ts +23 -0
  71. package/src/lib/model/ai-build-job.js +102 -13
  72. package/src/lib/model/ai-build-job.js.map +1 -1
  73. package/src/lib/model/ai-build-plan.d.ts +25 -0
  74. package/src/lib/model/ai-build-plan.js +45 -1
  75. package/src/lib/model/ai-build-plan.js.map +1 -1
  76. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  77. package/src/lib/model/ai-credit-estimate.js +51 -0
  78. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  79. package/src/lib/model/ai-job-activity.js +4 -1
  80. package/src/lib/model/ai-job-activity.js.map +1 -1
  81. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  82. package/src/lib/model/ai-jobs.types.js +4 -1
  83. package/src/lib/model/ai-jobs.types.js.map +1 -1
  84. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  85. package/src/lib/model/ai-layout-menus.js +518 -0
  86. package/src/lib/model/ai-layout-menus.js.map +1 -0
  87. package/src/lib/model/ai-products.d.ts +31 -1
  88. package/src/lib/model/ai-products.js +34 -1
  89. package/src/lib/model/ai-products.js.map +1 -1
  90. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  91. package/src/lib/model/ai-site-inventory.js.map +1 -1
  92. package/src/lib/model/ai-site-job.d.ts +10 -0
  93. package/src/lib/model/ai-site-job.js +36 -9
  94. package/src/lib/model/ai-site-job.js.map +1 -1
  95. package/src/lib/model/ai-site-store-pages.js +1 -1
  96. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  97. package/src/lib/runtime/ai-doctrine-validators.d.ts +53 -0
  98. package/src/lib/runtime/ai-doctrine-validators.js +166 -25
  99. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  100. package/src/lib/runtime/ai-doctrine.js +7 -2
  101. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  102. package/src/lib/runtime/ai-palette.generated.js +1 -1
  103. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  104. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  105. package/src/lib/runtime/ai-products-generation.js +12 -6
  106. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  107. package/src/lib/runtime/site-inventory.d.ts +12 -0
  108. package/src/lib/runtime/site-inventory.js +29 -1
  109. package/src/lib/runtime/site-inventory.js.map +1 -1
  110. package/src/lib/server.js +12 -0
  111. package/src/lib/server.js.map +1 -1
  112. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  113. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  114. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  115. package/src/lib/tools/ai-products-tool.js +15 -5
  116. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -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":"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"}
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;
@@ -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/** 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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"}
@@ -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
@@ -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
  }