@aglyn/plugins-ai 1.0.0-beta.243 → 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 (123) 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 +24 -6
  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 +5 -2
  30. package/src/lib/jobs/ai-job-page-language.js +38 -10
  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-page-step.js +11 -4
  35. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  36. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  37. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  38. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  39. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  40. package/src/lib/jobs/ai-job-products-step.js +2 -2
  41. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  43. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  44. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  45. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  46. package/src/lib/jobs/ai-job-site-content.js +28 -12
  47. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  48. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  49. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  50. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  51. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  52. package/src/lib/jobs/ai-job-site-step.js +86 -12
  53. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  54. package/src/lib/jobs/ai-site-publish.d.ts +76 -4
  55. package/src/lib/jobs/ai-site-publish.js +231 -59
  56. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  58. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  59. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  61. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  62. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  64. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  65. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  66. package/src/lib/layout-language/ai-layout-links.js +5 -2
  67. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  72. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  73. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  74. package/src/lib/model/ai-build-job.d.ts +23 -0
  75. package/src/lib/model/ai-build-job.js +102 -13
  76. package/src/lib/model/ai-build-job.js.map +1 -1
  77. package/src/lib/model/ai-build-plan.d.ts +25 -0
  78. package/src/lib/model/ai-build-plan.js +45 -1
  79. package/src/lib/model/ai-build-plan.js.map +1 -1
  80. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  81. package/src/lib/model/ai-credit-estimate.js +51 -0
  82. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  83. package/src/lib/model/ai-job-activity.js +4 -1
  84. package/src/lib/model/ai-job-activity.js.map +1 -1
  85. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  86. package/src/lib/model/ai-jobs.types.js +4 -1
  87. package/src/lib/model/ai-jobs.types.js.map +1 -1
  88. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  89. package/src/lib/model/ai-layout-menus.js +518 -0
  90. package/src/lib/model/ai-layout-menus.js.map +1 -0
  91. package/src/lib/model/ai-products.d.ts +31 -1
  92. package/src/lib/model/ai-products.js +34 -1
  93. package/src/lib/model/ai-products.js.map +1 -1
  94. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  95. package/src/lib/model/ai-site-inventory.js.map +1 -1
  96. package/src/lib/model/ai-site-job.d.ts +10 -0
  97. package/src/lib/model/ai-site-job.js +36 -9
  98. package/src/lib/model/ai-site-job.js.map +1 -1
  99. package/src/lib/model/ai-site-store-pages.js +1 -1
  100. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  101. package/src/lib/runtime/ai-doctrine-validators.d.ts +95 -1
  102. package/src/lib/runtime/ai-doctrine-validators.js +264 -36
  103. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  104. package/src/lib/runtime/ai-doctrine.js +7 -2
  105. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  106. package/src/lib/runtime/ai-palette.generated.js +1 -1
  107. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  108. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  109. package/src/lib/runtime/ai-products-generation.js +12 -6
  110. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  111. package/src/lib/runtime/site-inventory.d.ts +12 -0
  112. package/src/lib/runtime/site-inventory.js +29 -1
  113. package/src/lib/runtime/site-inventory.js.map +1 -1
  114. package/src/lib/server.js +12 -0
  115. package/src/lib/server.js.map +1 -1
  116. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  117. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  118. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  119. package/src/lib/tools/ai-products-tool.js +15 -5
  120. package/src/lib/tools/ai-products-tool.js.map +1 -1
  121. package/src/lib/usage/ai-quota-checks.d.ts +13 -3
  122. package/src/lib/usage/ai-quota-checks.js +46 -8
  123. package/src/lib/usage/ai-quota-checks.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/testing/stand-in-dataset-index.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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginRecordIndex,\n type PluginIndexedRecord,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { unregisterPluginServices } from '@aglyn/aglyn/plugin-manager/plugin-services'\n\n/**\n * The `dataset` index the plugin that keeps the workspace's datasets\n * publishes (AGL-3080), stood in over a spec's own Firestore double — this\n * plugin may not load the data plugin that registers the real one. It answers\n * in the shape the data plugin's `dataset-record-index.ts` documents: live\n * datasets only, narrowed to a site to those shared with it, each with its\n * fields (`{ id, name, type }` and a `customType` where one rides it, in\n * the dataset page's order) and its\n * `visibleTo`. What the data plugin itself answers is held in its own spec.\n */\n\nconst OWNER = 'data'\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A field as a dataset document stores it, as far as this stand-in reads one. */\ninterface StoredField {\n name?: unknown\n type?: unknown\n customType?: unknown\n}\n\n/**\n * The fields a dataset document declares, in the Data page's order: its\n * model's, or for a dataset from before models, each name in `fields` as a\n * text field — the derivation the data plugin applies.\n */\nfunction storedFields(data: Record<string, unknown>): { order: string[]; fields: Record<string, StoredField> } {\n const model = data['model'] as { order?: unknown; fields?: unknown } | undefined\n if (model && Array.isArray(model.order) && model.order.length && model.fields && typeof model.fields === 'object') {\n return { order: model.order.map(String), fields: model.fields as Record<string, StoredField> }\n }\n const names = Array.isArray(data['fields']) ? data['fields'].map(String) : []\n const titled = (name: string) => {\n const words = name.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : name\n }\n return {\n order: names,\n fields: Object.fromEntries(names.map((name) => [name, { name: titled(name), type: 'text' }])),\n }\n}\n\nfunction record(id: string, data: Record<string, unknown> | undefined): PluginIndexedRecord | null {\n if (!data || data['deletedAt'] != null) return null\n const model = storedFields(data)\n return {\n id,\n name: str(data['displayName']) || str(data['name']) || id,\n facts: {\n fields: model.order.map((fieldId) => ({\n id: fieldId,\n name: str(model.fields[fieldId]?.name) || fieldId,\n type: String(model.fields[fieldId]?.type ?? 'text'),\n ...(str(model.fields[fieldId]?.customType) ? { customType: str(model.fields[fieldId]?.customType) } : {}),\n })),\n visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : [],\n },\n }\n}\n\n/** Registers it, reading through `firestore`. */\nexport function standInDatasetIndex(firestore: FirebaseFirestore.Firestore): void {\n const datasets = (orgId: string) => firestore.collection('orgs').doc(orgId).collection('datasets')\n const shared = (one: PluginIndexedRecord | null, hostId: string | null | undefined) =>\n one && (!hostId || visibleToHost(one.facts['visibleTo'] as string[], hostId)) ? one : null\n registerPluginRecordIndex(\n 'dataset',\n {\n async list({ orgId, hostId, limit }) {\n if (!orgId) return { records: [], truncated: false }\n const snapshot = await datasets(orgId).get()\n const records = snapshot.docs\n .map((doc) => shared(record(doc.id, doc.data()), hostId))\n .filter((one): one is PluginIndexedRecord => one !== null)\n return { records: records.slice(0, limit), truncated: records.length > limit }\n },\n async get({ orgId, hostId, id }) {\n if (!orgId) return null\n const snapshot = await datasets(orgId).doc(id).get()\n return snapshot.exists ? shared(record(snapshot.id, snapshot.data()), hostId) : null\n },\n },\n { pluginId: OWNER },\n )\n}\n\n/** Forgets it, so the next spec starts with nothing keeping datasets. */\nexport function removeStandInDatasetIndex(): void {\n unregisterPluginServices(OWNER)\n}\n"],"names":["visibleToHost","registerPluginRecordIndex","unregisterPluginServices","OWNER","str","value","trim","storedFields","data","model","Array","isArray","order","length","fields","map","String","names","titled","name","words","replace","charAt","toUpperCase","slice","Object","fromEntries","type","record","id","facts","fieldId","customType","visibleTo","standInDatasetIndex","firestore","datasets","orgId","collection","doc","shared","one","hostId","list","limit","records","truncated","snapshot","get","docs","filter","exists","pluginId","removeStandInDatasetIndex"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/testing/stand-in-dataset-index.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 { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginRecordIndex,\n type PluginIndexedRecord,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { unregisterPluginServices } from '@aglyn/aglyn/plugin-manager/plugin-services'\n\n/**\n * The `dataset` index the plugin that keeps the workspace's datasets\n * publishes (AGL-3080), stood in over a spec's own Firestore double — this\n * plugin may not load the data plugin that registers the real one. It answers\n * in the shape the data plugin's `dataset-record-index.ts` documents: live\n * datasets only, narrowed to a site to those shared with it, each with its\n * fields (`{ id, name, type }` and a `customType` where one rides it, in\n * the dataset page's order) and its\n * `visibleTo`; narrowed to a site, the site's record page for it as\n * `recordPage` (`{ screenId, base, slugField }`, or `null`, AGL-3747) read off\n * `hosts/{hostId}/recordPages`. What the data plugin itself answers is held in\n * its own spec.\n */\n\nconst OWNER = 'data'\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A field as a dataset document stores it, as far as this stand-in reads one. */\ninterface StoredField {\n name?: unknown\n type?: unknown\n customType?: unknown\n}\n\n/**\n * The fields a dataset document declares, in the Data page's order: its\n * model's, or for a dataset from before models, each name in `fields` as a\n * text field — the derivation the data plugin applies.\n */\nfunction storedFields(data: Record<string, unknown>): { order: string[]; fields: Record<string, StoredField> } {\n const model = data['model'] as { order?: unknown; fields?: unknown } | undefined\n if (model && Array.isArray(model.order) && model.order.length && model.fields && typeof model.fields === 'object') {\n return { order: model.order.map(String), fields: model.fields as Record<string, StoredField> }\n }\n const names = Array.isArray(data['fields']) ? data['fields'].map(String) : []\n const titled = (name: string) => {\n const words = name.replace(/_/g, ' ').trim()\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : name\n }\n return {\n order: names,\n fields: Object.fromEntries(names.map((name) => [name, { name: titled(name), type: 'text' }])),\n }\n}\n\nfunction record(id: string, data: Record<string, unknown> | undefined): PluginIndexedRecord | null {\n if (!data || data['deletedAt'] != null) return null\n const model = storedFields(data)\n return {\n id,\n name: str(data['displayName']) || str(data['name']) || id,\n facts: {\n fields: model.order.map((fieldId) => ({\n id: fieldId,\n name: str(model.fields[fieldId]?.name) || fieldId,\n type: String(model.fields[fieldId]?.type ?? 'text'),\n ...(str(model.fields[fieldId]?.customType) ? { customType: str(model.fields[fieldId]?.customType) } : {}),\n })),\n visibleTo: Array.isArray(data['visibleTo']) ? data['visibleTo'] : [],\n },\n }\n}\n\n/** Registers it, reading through `firestore`. */\nexport function standInDatasetIndex(firestore: FirebaseFirestore.Firestore): void {\n const datasets = (orgId: string) => firestore.collection('orgs').doc(orgId).collection('datasets')\n const shared = (one: PluginIndexedRecord | null, hostId: string | null | undefined) =>\n one && (!hostId || visibleToHost(one.facts['visibleTo'] as string[], hostId)) ? one : null\n const recordPages = async (hostId: string) => {\n const snapshot = await firestore.collection('hosts').doc(hostId).collection('recordPages').get()\n const pages = new Map<string, { screenId: string; base: string; slugField: string }>()\n for (const doc of snapshot.docs) {\n const data = doc.data() ?? {}\n const base = str(data['base']).replace(/^\\/+|\\/+$/g, '')\n if (!str(data['datasetId']) || !base || !str(data['slugField']) || pages.has(str(data['datasetId']))) continue\n pages.set(str(data['datasetId']), { screenId: doc.id, base, slugField: str(data['slugField']) })\n }\n return pages\n }\n const withPages = async (records: PluginIndexedRecord[], hostId: string | null | undefined) => {\n if (!hostId || !records.length) return records\n const pages = await recordPages(hostId)\n return records.map((one) => ({ ...one, facts: { ...one.facts, recordPage: pages.get(one.id) ?? null } }))\n }\n registerPluginRecordIndex(\n 'dataset',\n {\n async list({ orgId, hostId, limit }) {\n if (!orgId) return { records: [], truncated: false }\n const snapshot = await datasets(orgId).get()\n const records = snapshot.docs\n .map((doc) => shared(record(doc.id, doc.data()), hostId))\n .filter((one): one is PluginIndexedRecord => one !== null)\n return { records: await withPages(records.slice(0, limit), hostId), truncated: records.length > limit }\n },\n async get({ orgId, hostId, id }) {\n if (!orgId) return null\n const snapshot = await datasets(orgId).doc(id).get()\n const one = snapshot.exists ? shared(record(snapshot.id, snapshot.data()), hostId) : null\n return one ? (await withPages([one], hostId))[0] : null\n },\n },\n { pluginId: OWNER },\n )\n}\n\n/** Forgets it, so the next spec starts with nothing keeping datasets. */\nexport function removeStandInDatasetIndex(): void {\n unregisterPluginServices(OWNER)\n}\n"],"names":["visibleToHost","registerPluginRecordIndex","unregisterPluginServices","OWNER","str","value","trim","storedFields","data","model","Array","isArray","order","length","fields","map","String","names","titled","name","words","replace","charAt","toUpperCase","slice","Object","fromEntries","type","record","id","facts","fieldId","customType","visibleTo","standInDatasetIndex","firestore","datasets","orgId","collection","doc","shared","one","hostId","recordPages","snapshot","get","pages","Map","docs","base","has","set","screenId","slugField","withPages","records","recordPage","list","limit","truncated","filter","exists","pluginId","removeStandInDatasetIndex"],"mappings":"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"}
@@ -28,7 +28,9 @@ import type { AiGenerationCheckResult } from '../runtime/ai-doctrine';
28
28
  * discounts, from a brief.
29
29
  *
30
30
  * Each schema is kept small, and no field is nullable: "none" is an empty
31
- * string, an empty list or zero. A strict schema accepts no length keyword,
31
+ * string, an empty list or zero. A catalog product's `price` is the one field
32
+ * no answer is refused for: code keeps it in range or drops it to the
33
+ * store's default (`aiCatalogPrice`). A strict schema accepts no length keyword,
32
34
  * so every bound is stated in a description and enforced here. A check
33
35
  * returns a value only when nothing in the answer is broken, and otherwise
34
36
  * the violations and the parts at fault, for the generation call's one
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { SEO_LISTING_FIELDS } from "@aglyn/aglyn/app-utils/seo-listing-fields";
17
- import { AI_CATALOG_DESCRIPTION_MAX_CHARS, AI_CATALOG_OPTION_VALUE_MAX_CHARS, AI_CATALOG_OPTION_VALUES_MAX, AI_CATALOG_OPTIONS_MAX, AI_CATALOG_PHOTO_MAX_CHARS, AI_CATALOG_PRODUCTS, AI_CATALOG_TAGS_MAX, AI_CATEGORIES_MAX, AI_CATEGORY_NAME_MAX_CHARS, AI_DISCOUNT_CODE, AI_DISCOUNT_MAX_FIXED_USD, AI_DISCOUNT_MAX_PERCENT, AI_DISCOUNTS_MAX, AI_PRODUCT_CATEGORIES_MAX, AI_PRODUCT_DESCRIPTION_MAX_CHARS, AI_PRODUCT_NAME_MAX_CHARS, AI_PRODUCT_OPTION_NAME_MAX_CHARS, AI_PRODUCT_TAG_MAX_CHARS, AI_PRODUCT_TAGS_MAX, AI_PRODUCT_TYPES, AI_PROPOSAL_WHY_MAX_CHARS } from "../model/ai-products.js";
17
+ import { AI_CATALOG_DESCRIPTION_MAX_CHARS, AI_CATALOG_OPTION_VALUE_MAX_CHARS, AI_CATALOG_OPTION_VALUES_MAX, AI_CATALOG_OPTIONS_MAX, AI_CATALOG_PHOTO_MAX_CHARS, AI_CATALOG_PRICE_USD, AI_CATALOG_PRODUCTS, AI_CATALOG_TAGS_MAX, AI_CATEGORIES_MAX, AI_CATEGORY_NAME_MAX_CHARS, AI_DISCOUNT_CODE, AI_DISCOUNT_MAX_FIXED_USD, AI_DISCOUNT_MAX_PERCENT, AI_DISCOUNTS_MAX, AI_PRODUCT_CATEGORIES_MAX, AI_PRODUCT_DESCRIPTION_MAX_CHARS, AI_PRODUCT_NAME_MAX_CHARS, AI_PRODUCT_OPTION_NAME_MAX_CHARS, AI_PRODUCT_TAG_MAX_CHARS, AI_PRODUCT_TAGS_MAX, AI_PRODUCT_TYPES, AI_PROPOSAL_WHY_MAX_CHARS, aiCatalogPrice } from "../model/ai-products.js";
18
18
  import { findInventedPrices, findStorefrontClaims, storefrontClaimViolations } from "../model/ai-storefront-claims.js";
19
19
  import { detectOffVoiceCopy } from "../runtime/ai-doctrine-validators.js";
20
20
  /**
@@ -28,7 +28,9 @@ import { detectOffVoiceCopy } from "../runtime/ai-doctrine-validators.js";
28
28
  * discounts, from a brief.
29
29
  *
30
30
  * Each schema is kept small, and no field is nullable: "none" is an empty
31
- * string, an empty list or zero. A strict schema accepts no length keyword,
31
+ * string, an empty list or zero. A catalog product's `price` is the one field
32
+ * no answer is refused for: code keeps it in range or drops it to the
33
+ * store's default (`aiCatalogPrice`). A strict schema accepts no length keyword,
32
34
  * so every bound is stated in a description and enforced here. A check
33
35
  * returns a value only when nothing in the answer is broken, and otherwise
34
36
  * the violations and the parts at fault, for the generation call's one
@@ -270,7 +272,8 @@ function fail(findings, at, code, message, value) {
270
272
  'options',
271
273
  'seoTitle',
272
274
  'seoDescription',
273
- 'photo'
275
+ 'photo',
276
+ 'price'
274
277
  ],
275
278
  properties: {
276
279
  name: string(`Distinct within the list, at most ${AI_PRODUCT_NAME_MAX_CHARS} characters.`),
@@ -300,7 +303,11 @@ function fail(findings, at, code, message, value) {
300
303
  },
301
304
  seoTitle: string(`At most ${SEO_TITLE_MAX} characters.`),
302
305
  seoDescription: string(`At most ${SEO_DESCRIPTION_MAX} characters.`),
303
- photo: string(`What the product photo should show, in one sentence of at most ${AI_CATALOG_PHOTO_MAX_CHARS} characters.`)
306
+ photo: string(`What the product photo should show, in one sentence of at most ${AI_CATALOG_PHOTO_MAX_CHARS} characters.`),
307
+ price: {
308
+ type: 'number',
309
+ description: `Its suggested price in US dollars, from ${AI_CATALOG_PRICE_USD.min} to ${AI_CATALOG_PRICE_USD.max}.`
310
+ }
304
311
  }
305
312
  }
306
313
  }
@@ -392,7 +399,10 @@ function fail(findings, at, code, message, value) {
392
399
  options,
393
400
  seoTitle,
394
401
  seoDescription,
395
- photo
402
+ photo,
403
+ // Kept by code, never re-asked (Zach, 2026-10-12): a price that is
404
+ // missing or out of range is `null`, and the store's default stands in.
405
+ price: aiCatalogPrice(entry['price'])
396
406
  });
397
407
  });
398
408
  findings.violations.push(...claimViolations(samples, context.merchantWords), ...detectOffVoiceCopy(samples, null));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-products-tool.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 { SEO_LISTING_FIELDS } from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport {\n AI_CATALOG_DESCRIPTION_MAX_CHARS,\n AI_CATALOG_OPTION_VALUE_MAX_CHARS,\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_CATALOG_OPTIONS_MAX,\n AI_CATALOG_PHOTO_MAX_CHARS,\n AI_CATALOG_PRODUCTS,\n AI_CATALOG_TAGS_MAX,\n AI_CATEGORIES_MAX,\n AI_CATEGORY_NAME_MAX_CHARS,\n AI_DISCOUNT_CODE,\n AI_DISCOUNT_MAX_FIXED_USD,\n AI_DISCOUNT_MAX_PERCENT,\n AI_DISCOUNTS_MAX,\n AI_PRODUCT_CATEGORIES_MAX,\n AI_PRODUCT_DESCRIPTION_MAX_CHARS,\n AI_PRODUCT_NAME_MAX_CHARS,\n AI_PRODUCT_OPTION_NAME_MAX_CHARS,\n AI_PRODUCT_TAG_MAX_CHARS,\n AI_PRODUCT_TAGS_MAX,\n AI_PRODUCT_TYPES,\n AI_PROPOSAL_WHY_MAX_CHARS,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiDiscountKind,\n type AiProductCopy,\n type AiProductOption,\n type AiProductType,\n type AiProposedDiscount,\n} from '../model/ai-products'\nimport {\n findInventedPrices,\n findStorefrontClaims,\n storefrontClaimViolations,\n type AiStorefrontCopySample,\n} from '../model/ai-storefront-claims'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport { detectOffVoiceCopy, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tools commerce by AI answers through (AGL-2916), and the checks\n * that hold each answer to the commerce editor it proposes for.\n *\n * - `propose_product_copy` — one product's description, search listing, tags,\n * categories and option names.\n * - `propose_catalog` — a store's first products, from a brief.\n * - `propose_categories_and_discounts` — store categories and a first set of\n * discounts, from a brief.\n *\n * Each schema is kept small, and no field is nullable: \"none\" is an empty\n * string, an empty list or zero. A strict schema accepts no length keyword,\n * so every bound is stated in a description and enforced here. A check\n * returns a value only when nothing in the answer is broken, and otherwise\n * the violations and the parts at fault, for the generation call's one\n * re-ask.\n */\n\nexport const AI_PRODUCT_COPY_TOOL_NAME = 'propose_product_copy'\nexport const AI_CATALOG_TOOL_NAME = 'propose_catalog'\nexport const AI_CATEGORIES_TOOL_NAME = 'propose_categories_and_discounts'\n\nconst SEO_TITLE_MAX = SEO_LISTING_FIELDS.title.maxLength\nconst SEO_DESCRIPTION_MAX = SEO_LISTING_FIELDS.description.maxLength\n\nconst string = (description: string) => ({ type: 'string', description })\nconst strings = (description: string) => ({ type: 'array', items: { type: 'string' }, description })\n\n/* ------------------------------------------------------------------------ *\n * Shared reads\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** One line: runs of white space folded to one space. */\nconst line = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\n/** Paragraphs: spaces folded within a line, at most one blank line between paragraphs. */\nconst paragraphs = (value: unknown): string =>\n typeof value === 'string'\n ? value\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n')\n .map((part) => part.replace(/[ \\t\\f\\v]+/g, ' ').trim())\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n : ''\n\nconst MARKUP = /<\\/?[a-z][^>]*>/i\nconst MARKDOWN = /(^|\\n)\\s*#{1,6}\\s|\\*\\*[^*\\n]+\\*\\*|__[^_\\n]+__|!\\[[^\\]]*\\]\\(|\\[[^\\]]+\\]\\(https?:/\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n\ninterface Findings {\n violations: AiDoctrineViolation[]\n offending: Record<string, unknown>\n}\n\nfunction fail(findings: Findings, at: string, code: string, message: string, value: unknown): void {\n findings.violations.push({ rule: null, code, message, paths: [at] })\n if (!(at in findings.offending)) findings.offending[at] = value\n}\n\n/** A piece of copy a shopper reads: there, within its length, and plain text. */\nfunction checkCopyText(\n findings: Findings,\n at: string,\n raw: unknown,\n bounds: { label: string; max: number; multiline?: boolean; brackets?: boolean },\n): string {\n if (typeof raw !== 'string') {\n fail(findings, at, 'type', `${bounds.label} must be text.`, raw)\n return ''\n }\n const value = bounds.multiline ? paragraphs(raw) : line(raw)\n if (!value) {\n fail(findings, at, 'missing', `${bounds.label} is required.`, raw)\n return ''\n }\n if (value.length > bounds.max) {\n fail(findings, at, 'too-long', `${bounds.label} is ${value.length} characters; it may have ${bounds.max}.`, raw)\n }\n if (MARKUP.test(value) || MARKDOWN.test(value)) {\n fail(findings, at, 'markup', `${bounds.label} is plain text, with no markup or formatting marks.`, raw)\n }\n if (EMOJI.test(value)) fail(findings, at, 'emoji', `${bounds.label} holds no emoji.`, raw)\n if (URL.test(value)) fail(findings, at, 'link', `${bounds.label} holds no link.`, raw)\n if (bounds.brackets === false && BRACKET.test(value)) {\n fail(findings, at, 'bracket', `${bounds.label} is published as written, so it marks no missing fact in brackets.`, raw)\n }\n return value\n}\n\n/** Tags a shopper filters by: short, distinct, no `#`, no more than `max`. */\nfunction checkTags(findings: Findings, at: string, raw: unknown, max = AI_PRODUCT_TAGS_MAX): string[] {\n if (!Array.isArray(raw)) {\n fail(findings, at, 'type', 'Tags are a list of words or short phrases.', raw)\n return []\n }\n const seen = new Set<string>()\n const tags: string[] = []\n raw.forEach((entry, index) => {\n const tag = line(entry).replace(/^#+/, '').trim()\n if (!tag) return\n if (tag.length > AI_PRODUCT_TAG_MAX_CHARS || EMOJI.test(tag) || BRACKET.test(tag)) {\n fail(findings, `${at}[${index}]`, 'tag', `A tag is a plain word or phrase of at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`, entry)\n return\n }\n const key = tag.toLowerCase()\n if (seen.has(key)) return\n seen.add(key)\n tags.push(tag)\n })\n if (tags.length > max) {\n fail(findings, at, 'too-many', `A product carries at most ${max} tags.`, raw)\n }\n return tags\n}\n\n/** The claims and prices a set of copy may not make, as violations. */\nfunction claimViolations(samples: readonly AiStorefrontCopySample[], merchantWords: string): AiDoctrineViolation[] {\n return storefrontClaimViolations(\n findStorefrontClaims(samples, merchantWords),\n findInventedPrices(samples, merchantWords),\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * One product's copy\n * ------------------------------------------------------------------------ */\n\nexport const AI_PRODUCT_COPY_TOOL: AiTool = {\n name: AI_PRODUCT_COPY_TOOL_NAME,\n description: 'Propose the storefront copy for one product. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['description', 'seoTitle', 'seoDescription', 'tags', 'categoryIds', 'optionNames'],\n properties: {\n description: string(\n `The product description shoppers read, in short paragraphs, at most ${AI_PRODUCT_DESCRIPTION_MAX_CHARS} characters.`,\n ),\n seoTitle: string(`The search result headline, published as written, at most ${SEO_TITLE_MAX} characters.`),\n seoDescription: string(`The search result summary, at most ${SEO_DESCRIPTION_MAX} characters.`),\n tags: strings(`At most ${AI_PRODUCT_TAGS_MAX} tags, each at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`),\n categoryIds: strings(\n `At most ${AI_PRODUCT_CATEGORIES_MAX} category ids from the list you were given; empty when none fits.`,\n ),\n optionNames: strings(\n `One name per option the product has, in order, each at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters; empty when it has none.`,\n ),\n },\n },\n}\n\nexport interface AiProductCopyCheckContext {\n /** The ids of the site's categories the request listed. */\n categoryIds: readonly string[]\n /** How many options the product has. */\n optionCount: number\n /** The merchant's own words about the product: its name, text and tags. */\n merchantWords: string\n}\n\n/**\n * A product copy answer held to the editor: every field written and within\n * its length, plain text, the categories the request listed, one distinct\n * name per option, and no claim or price the copy may not make.\n */\nexport function checkAiProductCopy(\n answer: Record<string, unknown>,\n context: AiProductCopyCheckContext,\n): AiGenerationCheckResult<AiProductCopy> {\n const findings: Findings = { violations: [], offending: {} }\n const description = checkCopyText(findings, 'description', answer['description'], {\n label: 'The description',\n max: AI_PRODUCT_DESCRIPTION_MAX_CHARS,\n multiline: true,\n })\n const seoTitle = checkCopyText(findings, 'seoTitle', answer['seoTitle'], {\n label: 'The search title',\n max: SEO_TITLE_MAX,\n brackets: false,\n })\n const seoDescription = checkCopyText(findings, 'seoDescription', answer['seoDescription'], {\n label: 'The search description',\n max: SEO_DESCRIPTION_MAX,\n brackets: false,\n })\n const tags = checkTags(findings, 'tags', answer['tags'])\n\n const allowed = new Set(context.categoryIds)\n const categoryIds: string[] = []\n const rawCategories = answer['categoryIds']\n if (!Array.isArray(rawCategories)) {\n fail(findings, 'categoryIds', 'type', 'Categories are a list of ids.', rawCategories)\n } else {\n for (const entry of rawCategories) {\n const id = line(entry)\n if (!id || categoryIds.includes(id)) continue\n if (!allowed.has(id)) {\n fail(findings, 'categoryIds', 'category-unknown', 'Use only the category ids you were given.', rawCategories)\n continue\n }\n categoryIds.push(id)\n }\n if (categoryIds.length > AI_PRODUCT_CATEGORIES_MAX) {\n fail(findings, 'categoryIds', 'too-many', `A product goes in at most ${AI_PRODUCT_CATEGORIES_MAX} categories.`, rawCategories)\n }\n }\n\n const optionNames: string[] = []\n const rawOptions = answer['optionNames']\n if (!Array.isArray(rawOptions)) {\n fail(findings, 'optionNames', 'type', 'Option names are a list.', rawOptions)\n } else {\n if (rawOptions.length !== context.optionCount) {\n fail(\n findings,\n 'optionNames',\n 'option-count',\n `The product has ${context.optionCount} ${context.optionCount === 1 ? 'option' : 'options'}; give one name for each.`,\n rawOptions,\n )\n }\n rawOptions.slice(0, context.optionCount).forEach((entry, index) => {\n const name = line(entry)\n if (!name || name.length > AI_PRODUCT_OPTION_NAME_MAX_CHARS || BRACKET.test(name) || EMOJI.test(name)) {\n fail(findings, `optionNames[${index}]`, 'option-name', `An option name is a plain word or phrase of at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`, entry)\n }\n optionNames.push(name)\n })\n if (new Set(optionNames.map((name) => name.toLowerCase())).size !== optionNames.length) {\n fail(findings, 'optionNames', 'option-duplicate', 'Each option needs its own name.', rawOptions)\n }\n }\n\n const samples: AiStorefrontCopySample[] = [\n { at: 'description', text: description },\n { at: 'seoTitle', text: seoTitle },\n { at: 'seoDescription', text: seoDescription },\n ...tags.map((tag, index) => ({ at: `tags[${index}]`, text: tag })),\n ]\n findings.violations.push(\n ...claimViolations(samples, context.merchantWords),\n ...detectOffVoiceCopy(samples, null),\n )\n const value = findings.violations.length\n ? null\n : { description, seoTitle, seoDescription, tags, categoryIds, optionNames }\n return { value, violations: findings.violations, offending: findings.offending }\n}\n\n/* ------------------------------------------------------------------------ *\n * A store's first products\n * ------------------------------------------------------------------------ */\n\nexport const AI_CATALOG_TOOL: AiTool = {\n name: AI_CATALOG_TOOL_NAME,\n description: 'Propose the products of a new store, each a draft its owner completes. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['products'],\n properties: {\n products: {\n type: 'array',\n description: `Between ${AI_CATALOG_PRODUCTS.min} and ${AI_CATALOG_PRODUCTS.max} products.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type', 'description', 'tags', 'options', 'seoTitle', 'seoDescription', 'photo'],\n properties: {\n name: string(`Distinct within the list, at most ${AI_PRODUCT_NAME_MAX_CHARS} characters.`),\n type: { type: 'string', enum: [...AI_PRODUCT_TYPES] },\n description: string(`In short paragraphs, at most ${AI_CATALOG_DESCRIPTION_MAX_CHARS} characters.`),\n tags: strings(`At most ${AI_CATALOG_TAGS_MAX} tags, each at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`),\n options: {\n type: 'array',\n description: `The choices a shopper makes, at most ${AI_CATALOG_OPTIONS_MAX}; empty for a product with none.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'values'],\n properties: {\n name: string(`At most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`),\n values: strings(\n `At most ${AI_CATALOG_OPTION_VALUES_MAX} distinct values, each at most ${AI_CATALOG_OPTION_VALUE_MAX_CHARS} characters.`,\n ),\n },\n },\n },\n seoTitle: string(`At most ${SEO_TITLE_MAX} characters.`),\n seoDescription: string(`At most ${SEO_DESCRIPTION_MAX} characters.`),\n photo: string(`What the product photo should show, in one sentence of at most ${AI_CATALOG_PHOTO_MAX_CHARS} characters.`),\n },\n },\n },\n },\n },\n}\n\n/** A catalog answer held to the products editor and the storefront copy rules. */\nexport function checkAiCatalog(\n answer: Record<string, unknown>,\n context: { merchantWords: string },\n): AiGenerationCheckResult<AiCatalogProduct[]> {\n const findings: Findings = { violations: [], offending: {} }\n const raw = answer['products']\n if (!Array.isArray(raw)) {\n fail(findings, 'products', 'type', 'The products are a list.', raw)\n return { value: null, violations: findings.violations, offending: findings.offending }\n }\n if (raw.length < AI_CATALOG_PRODUCTS.min || raw.length > AI_CATALOG_PRODUCTS.max) {\n fail(findings, 'products', 'product-count', `Propose between ${AI_CATALOG_PRODUCTS.min} and ${AI_CATALOG_PRODUCTS.max} products.`, `${raw.length} products`)\n }\n const names = new Set<string>()\n const products: AiCatalogProduct[] = []\n const samples: AiStorefrontCopySample[] = []\n raw.slice(0, AI_CATALOG_PRODUCTS.max).forEach((entry, index) => {\n const at = `products[${index}]`\n if (!isRecord(entry)) {\n fail(findings, at, 'type', 'Each product is an object.', entry)\n return\n }\n const name = checkCopyText(findings, `${at}.name`, entry['name'], {\n label: 'A product name',\n max: AI_PRODUCT_NAME_MAX_CHARS,\n brackets: false,\n })\n if (name && names.has(name.toLowerCase())) {\n fail(findings, `${at}.name`, 'name-duplicate', 'Each product needs its own name.', entry['name'])\n }\n names.add(name.toLowerCase())\n const type = entry['type']\n if (!(AI_PRODUCT_TYPES as readonly unknown[]).includes(type)) {\n fail(findings, `${at}.type`, 'type', 'A product is physical, digital or a service.', type)\n }\n const description = checkCopyText(findings, `${at}.description`, entry['description'], {\n label: 'A product description',\n max: AI_CATALOG_DESCRIPTION_MAX_CHARS,\n multiline: true,\n })\n const tags = checkTags(findings, `${at}.tags`, entry['tags'], AI_CATALOG_TAGS_MAX)\n const options = checkCatalogOptions(findings, `${at}.options`, entry['options'])\n const seoTitle = checkCopyText(findings, `${at}.seoTitle`, entry['seoTitle'], {\n label: 'A search title',\n max: SEO_TITLE_MAX,\n brackets: false,\n })\n const seoDescription = checkCopyText(findings, `${at}.seoDescription`, entry['seoDescription'], {\n label: 'A search description',\n max: SEO_DESCRIPTION_MAX,\n brackets: false,\n })\n const photo = checkCopyText(findings, `${at}.photo`, entry['photo'], {\n label: 'What the photo shows',\n max: AI_CATALOG_PHOTO_MAX_CHARS,\n brackets: false,\n })\n samples.push(\n { at: `${at}.name`, text: name },\n { at: `${at}.description`, text: description },\n { at: `${at}.seoTitle`, text: seoTitle },\n { at: `${at}.seoDescription`, text: seoDescription },\n ...tags.map((tag, tagIndex) => ({ at: `${at}.tags[${tagIndex}]`, text: tag })),\n )\n products.push({\n name,\n type: type as AiProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n photo,\n })\n })\n findings.violations.push(...claimViolations(samples, context.merchantWords), ...detectOffVoiceCopy(samples, null))\n return {\n value: findings.violations.length ? null : products,\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n\nfunction checkCatalogOptions(findings: Findings, at: string, raw: unknown): AiProductOption[] {\n if (!Array.isArray(raw)) {\n fail(findings, at, 'type', 'Options are a list.', raw)\n return []\n }\n if (raw.length > AI_CATALOG_OPTIONS_MAX) {\n fail(findings, at, 'too-many', `A proposed product has at most ${AI_CATALOG_OPTIONS_MAX} options.`, raw)\n }\n const options: AiProductOption[] = []\n raw.slice(0, AI_CATALOG_OPTIONS_MAX).forEach((entry, index) => {\n const here = `${at}[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = line(record['name'])\n if (!name || name.length > AI_PRODUCT_OPTION_NAME_MAX_CHARS || BRACKET.test(name)) {\n fail(findings, `${here}.name`, 'option-name', `An option name is a plain word or phrase of at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`, record['name'])\n }\n const values = Array.isArray(record['values']) ? record['values'].map(line).filter(Boolean) : []\n if (!values.length || values.length > AI_CATALOG_OPTION_VALUES_MAX) {\n fail(findings, `${here}.values`, 'too-many', `An option has between 1 and ${AI_CATALOG_OPTION_VALUES_MAX} values.`, record['values'])\n }\n if (values.some((value) => value.length > AI_CATALOG_OPTION_VALUE_MAX_CHARS || BRACKET.test(value))) {\n fail(findings, `${here}.values`, 'option-values', `An option value is a plain word or phrase of at most ${AI_CATALOG_OPTION_VALUE_MAX_CHARS} characters.`, record['values'])\n }\n if (new Set(values.map((value) => value.toLowerCase())).size !== values.length) {\n fail(findings, `${here}.values`, 'option-values', 'An option’s values are all different.', record['values'])\n }\n options.push({ name, values: values.slice(0, AI_CATALOG_OPTION_VALUES_MAX) })\n })\n if (new Set(options.map((option) => option.name.toLowerCase())).size !== options.length) {\n fail(findings, at, 'option-duplicate', 'Each option needs its own name.', raw)\n }\n return options\n}\n\n/* ------------------------------------------------------------------------ *\n * Categories and discounts\n * ------------------------------------------------------------------------ */\n\nexport const AI_CATEGORIES_TOOL: AiTool = {\n name: AI_CATEGORIES_TOOL_NAME,\n description: 'Propose store categories and a first set of discounts, each created only if the owner accepts it. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['categories', 'discounts'],\n properties: {\n categories: {\n type: 'array',\n description: `At most ${AI_CATEGORIES_MAX} categories; empty when the brief gives no reason for any.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'why'],\n properties: {\n name: string(`At most ${AI_CATEGORY_NAME_MAX_CHARS} characters.`),\n why: string(`One sentence of at most ${AI_PROPOSAL_WHY_MAX_CHARS} characters.`),\n },\n },\n },\n discounts: {\n type: 'array',\n description: `At most ${AI_DISCOUNTS_MAX} discounts; empty when the brief gives no reason for any.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'code', 'kind', 'value', 'minimumOrderUsd', 'why'],\n properties: {\n name: string(`At most ${AI_CATEGORY_NAME_MAX_CHARS} characters.`),\n code: string('The code a shopper types: 3 to 20 capital letters, digits, dashes or underscores; empty for a discount that applies on its own.'),\n kind: { type: 'string', enum: ['percent', 'fixed', 'free_shipping'] },\n value: {\n type: 'number',\n description: `percent: the percentage, 1 to ${AI_DISCOUNT_MAX_PERCENT}; fixed: dollars off, at most ${AI_DISCOUNT_MAX_FIXED_USD}; free_shipping: 0.`,\n },\n minimumOrderUsd: { type: 'number', description: 'The smallest order it applies to, in dollars; 0 for any order.' },\n why: string(`One sentence of at most ${AI_PROPOSAL_WHY_MAX_CHARS} characters.`),\n },\n },\n },\n },\n },\n}\n\n/** A categories answer held to the catalog and discount editors and the storefront copy rules. */\nexport function checkAiCategories(\n answer: Record<string, unknown>,\n context: { existingCategoryNames: readonly string[]; merchantWords: string },\n): AiGenerationCheckResult<Pick<AiCategoriesProposal, 'categories' | 'discounts'>> {\n const findings: Findings = { violations: [], offending: {} }\n const existing = new Set(context.existingCategoryNames.map((name) => line(name).toLowerCase()))\n const samples: AiStorefrontCopySample[] = []\n\n const categories: AiCategoriesProposal['categories'] = []\n const rawCategories = answer['categories']\n if (!Array.isArray(rawCategories)) {\n fail(findings, 'categories', 'type', 'Categories are a list.', rawCategories)\n } else {\n if (rawCategories.length > AI_CATEGORIES_MAX) {\n fail(findings, 'categories', 'too-many', `Propose at most ${AI_CATEGORIES_MAX} categories.`, `${rawCategories.length} categories`)\n }\n const seen = new Set<string>()\n rawCategories.slice(0, AI_CATEGORIES_MAX).forEach((entry, index) => {\n const at = `categories[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = checkCopyText(findings, `${at}.name`, record['name'], {\n label: 'A category name',\n max: AI_CATEGORY_NAME_MAX_CHARS,\n brackets: false,\n })\n const key = name.toLowerCase()\n if (name && existing.has(key)) {\n fail(findings, `${at}.name`, 'category-exists', `The store already has a category named ${name}.`, record['name'])\n } else if (name && seen.has(key)) {\n fail(findings, `${at}.name`, 'category-duplicate', 'Each category needs its own name.', record['name'])\n }\n seen.add(key)\n const why = checkCopyText(findings, `${at}.why`, record['why'], {\n label: 'Why a category helps',\n max: AI_PROPOSAL_WHY_MAX_CHARS,\n brackets: false,\n })\n samples.push({ at: `${at}.name`, text: name }, { at: `${at}.why`, text: why })\n categories.push({ name, why })\n })\n }\n\n const discounts: AiProposedDiscount[] = []\n const rawDiscounts = answer['discounts']\n if (!Array.isArray(rawDiscounts)) {\n fail(findings, 'discounts', 'type', 'Discounts are a list.', rawDiscounts)\n } else {\n if (rawDiscounts.length > AI_DISCOUNTS_MAX) {\n fail(findings, 'discounts', 'too-many', `Propose at most ${AI_DISCOUNTS_MAX} discounts.`, `${rawDiscounts.length} discounts`)\n }\n const codes = new Set<string>()\n rawDiscounts.slice(0, AI_DISCOUNTS_MAX).forEach((entry, index) => {\n const at = `discounts[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = checkCopyText(findings, `${at}.name`, record['name'], {\n label: 'A discount name',\n max: AI_CATEGORY_NAME_MAX_CHARS,\n brackets: false,\n })\n const typedCode = line(record['code']).toUpperCase()\n if (typedCode && !AI_DISCOUNT_CODE.test(typedCode)) {\n fail(findings, `${at}.code`, 'code', 'A code is 3 to 20 capital letters, digits, dashes or underscores.', record['code'])\n } else if (typedCode && codes.has(typedCode)) {\n fail(findings, `${at}.code`, 'code-duplicate', 'Each discount needs its own code.', record['code'])\n }\n if (typedCode) codes.add(typedCode)\n const kind = record['kind']\n const value = Number(record['value'])\n const minimum = Number(record['minimumOrderUsd'])\n let valuePct: number | null = null\n let valueCents: number | null = null\n if (kind === 'percent') {\n if (!Number.isInteger(value) || value < 1 || value > AI_DISCOUNT_MAX_PERCENT) {\n fail(findings, `${at}.value`, 'discount-value', `A percentage is a whole number from 1 to ${AI_DISCOUNT_MAX_PERCENT}.`, record['value'])\n }\n valuePct = value\n } else if (kind === 'fixed') {\n if (!Number.isFinite(value) || value <= 0 || value > AI_DISCOUNT_MAX_FIXED_USD) {\n fail(findings, `${at}.value`, 'discount-value', `An amount off is more than 0 and at most ${AI_DISCOUNT_MAX_FIXED_USD} dollars.`, record['value'])\n }\n valueCents = Math.round(value * 100)\n } else if (kind !== 'free_shipping') {\n fail(findings, `${at}.kind`, 'type', 'A discount is a percentage, an amount off or free shipping.', kind)\n }\n if (!Number.isFinite(minimum) || minimum < 0 || minimum > 10_000) {\n fail(findings, `${at}.minimumOrderUsd`, 'discount-minimum', 'A minimum order is between 0 and 10,000 dollars.', record['minimumOrderUsd'])\n }\n const why = checkCopyText(findings, `${at}.why`, record['why'], {\n label: 'Why a discount helps',\n max: AI_PROPOSAL_WHY_MAX_CHARS,\n brackets: false,\n })\n // The discount's own amount is stated in its fields, so only its name\n // and reason are read for a claim: \"15% off\" is the discount, not a price.\n samples.push({ at: `${at}.name`, text: name }, { at: `${at}.why`, text: why })\n discounts.push({\n name,\n code: typedCode || null,\n kind: kind as AiDiscountKind,\n valuePct,\n valueCents,\n minSubtotalCents: minimum > 0 ? Math.round(minimum * 100) : null,\n why,\n })\n })\n }\n\n if (!categories.length && !discounts.length && !findings.violations.length) {\n fail(findings, 'categories', 'empty', 'Propose at least one category or one discount.', answer)\n }\n findings.violations.push(\n ...storefrontClaimViolations(findStorefrontClaims(samples, context.merchantWords)),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { categories, discounts },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["SEO_LISTING_FIELDS","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRODUCTS","AI_CATALOG_TAGS_MAX","AI_CATEGORIES_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_DISCOUNT_CODE","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNTS_MAX","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TYPES","AI_PROPOSAL_WHY_MAX_CHARS","findInventedPrices","findStorefrontClaims","storefrontClaimViolations","detectOffVoiceCopy","AI_PRODUCT_COPY_TOOL_NAME","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL_NAME","SEO_TITLE_MAX","title","maxLength","SEO_DESCRIPTION_MAX","description","string","type","strings","items","isRecord","value","Array","isArray","line","replace","trim","paragraphs","split","map","part","join","MARKUP","MARKDOWN","EMOJI","BRACKET","URL","fail","findings","at","code","message","violations","push","rule","paths","offending","checkCopyText","raw","bounds","label","multiline","length","max","test","brackets","checkTags","seen","Set","tags","forEach","entry","index","tag","key","toLowerCase","has","add","claimViolations","samples","merchantWords","AI_PRODUCT_COPY_TOOL","name","strict","inputSchema","additionalProperties","required","properties","seoTitle","seoDescription","categoryIds","optionNames","checkAiProductCopy","answer","context","allowed","rawCategories","id","includes","rawOptions","optionCount","slice","size","text","AI_CATALOG_TOOL","products","min","enum","options","values","photo","checkAiCatalog","names","checkCatalogOptions","tagIndex","here","record","filter","Boolean","some","option","AI_CATEGORIES_TOOL","categories","why","discounts","kind","minimumOrderUsd","checkAiCategories","existing","existingCategoryNames","rawDiscounts","codes","typedCode","toUpperCase","Number","minimum","valuePct","valueCents","isInteger","isFinite","Math","round","minSubtotalCents"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-products-tool.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 { SEO_LISTING_FIELDS } from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport {\n AI_CATALOG_DESCRIPTION_MAX_CHARS,\n AI_CATALOG_OPTION_VALUE_MAX_CHARS,\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_CATALOG_OPTIONS_MAX,\n AI_CATALOG_PHOTO_MAX_CHARS,\n AI_CATALOG_PRICE_USD,\n AI_CATALOG_PRODUCTS,\n AI_CATALOG_TAGS_MAX,\n AI_CATEGORIES_MAX,\n AI_CATEGORY_NAME_MAX_CHARS,\n AI_DISCOUNT_CODE,\n AI_DISCOUNT_MAX_FIXED_USD,\n AI_DISCOUNT_MAX_PERCENT,\n AI_DISCOUNTS_MAX,\n AI_PRODUCT_CATEGORIES_MAX,\n AI_PRODUCT_DESCRIPTION_MAX_CHARS,\n AI_PRODUCT_NAME_MAX_CHARS,\n AI_PRODUCT_OPTION_NAME_MAX_CHARS,\n AI_PRODUCT_TAG_MAX_CHARS,\n AI_PRODUCT_TAGS_MAX,\n AI_PRODUCT_TYPES,\n AI_PROPOSAL_WHY_MAX_CHARS,\n aiCatalogPrice,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiDiscountKind,\n type AiProductCopy,\n type AiProductOption,\n type AiProductType,\n type AiProposedDiscount,\n} from '../model/ai-products'\nimport {\n findInventedPrices,\n findStorefrontClaims,\n storefrontClaimViolations,\n type AiStorefrontCopySample,\n} from '../model/ai-storefront-claims'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport { detectOffVoiceCopy, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tools commerce by AI answers through (AGL-2916), and the checks\n * that hold each answer to the commerce editor it proposes for.\n *\n * - `propose_product_copy` — one product's description, search listing, tags,\n * categories and option names.\n * - `propose_catalog` — a store's first products, from a brief.\n * - `propose_categories_and_discounts` — store categories and a first set of\n * discounts, from a brief.\n *\n * Each schema is kept small, and no field is nullable: \"none\" is an empty\n * string, an empty list or zero. A catalog product's `price` is the one field\n * no answer is refused for: code keeps it in range or drops it to the\n * store's default (`aiCatalogPrice`). A strict schema accepts no length keyword,\n * so every bound is stated in a description and enforced here. A check\n * returns a value only when nothing in the answer is broken, and otherwise\n * the violations and the parts at fault, for the generation call's one\n * re-ask.\n */\n\nexport const AI_PRODUCT_COPY_TOOL_NAME = 'propose_product_copy'\nexport const AI_CATALOG_TOOL_NAME = 'propose_catalog'\nexport const AI_CATEGORIES_TOOL_NAME = 'propose_categories_and_discounts'\n\nconst SEO_TITLE_MAX = SEO_LISTING_FIELDS.title.maxLength\nconst SEO_DESCRIPTION_MAX = SEO_LISTING_FIELDS.description.maxLength\n\nconst string = (description: string) => ({ type: 'string', description })\nconst strings = (description: string) => ({ type: 'array', items: { type: 'string' }, description })\n\n/* ------------------------------------------------------------------------ *\n * Shared reads\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** One line: runs of white space folded to one space. */\nconst line = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\n/** Paragraphs: spaces folded within a line, at most one blank line between paragraphs. */\nconst paragraphs = (value: unknown): string =>\n typeof value === 'string'\n ? value\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n')\n .map((part) => part.replace(/[ \\t\\f\\v]+/g, ' ').trim())\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n : ''\n\nconst MARKUP = /<\\/?[a-z][^>]*>/i\nconst MARKDOWN = /(^|\\n)\\s*#{1,6}\\s|\\*\\*[^*\\n]+\\*\\*|__[^_\\n]+__|!\\[[^\\]]*\\]\\(|\\[[^\\]]+\\]\\(https?:/\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n\ninterface Findings {\n violations: AiDoctrineViolation[]\n offending: Record<string, unknown>\n}\n\nfunction fail(findings: Findings, at: string, code: string, message: string, value: unknown): void {\n findings.violations.push({ rule: null, code, message, paths: [at] })\n if (!(at in findings.offending)) findings.offending[at] = value\n}\n\n/** A piece of copy a shopper reads: there, within its length, and plain text. */\nfunction checkCopyText(\n findings: Findings,\n at: string,\n raw: unknown,\n bounds: { label: string; max: number; multiline?: boolean; brackets?: boolean },\n): string {\n if (typeof raw !== 'string') {\n fail(findings, at, 'type', `${bounds.label} must be text.`, raw)\n return ''\n }\n const value = bounds.multiline ? paragraphs(raw) : line(raw)\n if (!value) {\n fail(findings, at, 'missing', `${bounds.label} is required.`, raw)\n return ''\n }\n if (value.length > bounds.max) {\n fail(findings, at, 'too-long', `${bounds.label} is ${value.length} characters; it may have ${bounds.max}.`, raw)\n }\n if (MARKUP.test(value) || MARKDOWN.test(value)) {\n fail(findings, at, 'markup', `${bounds.label} is plain text, with no markup or formatting marks.`, raw)\n }\n if (EMOJI.test(value)) fail(findings, at, 'emoji', `${bounds.label} holds no emoji.`, raw)\n if (URL.test(value)) fail(findings, at, 'link', `${bounds.label} holds no link.`, raw)\n if (bounds.brackets === false && BRACKET.test(value)) {\n fail(findings, at, 'bracket', `${bounds.label} is published as written, so it marks no missing fact in brackets.`, raw)\n }\n return value\n}\n\n/** Tags a shopper filters by: short, distinct, no `#`, no more than `max`. */\nfunction checkTags(findings: Findings, at: string, raw: unknown, max = AI_PRODUCT_TAGS_MAX): string[] {\n if (!Array.isArray(raw)) {\n fail(findings, at, 'type', 'Tags are a list of words or short phrases.', raw)\n return []\n }\n const seen = new Set<string>()\n const tags: string[] = []\n raw.forEach((entry, index) => {\n const tag = line(entry).replace(/^#+/, '').trim()\n if (!tag) return\n if (tag.length > AI_PRODUCT_TAG_MAX_CHARS || EMOJI.test(tag) || BRACKET.test(tag)) {\n fail(findings, `${at}[${index}]`, 'tag', `A tag is a plain word or phrase of at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`, entry)\n return\n }\n const key = tag.toLowerCase()\n if (seen.has(key)) return\n seen.add(key)\n tags.push(tag)\n })\n if (tags.length > max) {\n fail(findings, at, 'too-many', `A product carries at most ${max} tags.`, raw)\n }\n return tags\n}\n\n/** The claims and prices a set of copy may not make, as violations. */\nfunction claimViolations(samples: readonly AiStorefrontCopySample[], merchantWords: string): AiDoctrineViolation[] {\n return storefrontClaimViolations(\n findStorefrontClaims(samples, merchantWords),\n findInventedPrices(samples, merchantWords),\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * One product's copy\n * ------------------------------------------------------------------------ */\n\nexport const AI_PRODUCT_COPY_TOOL: AiTool = {\n name: AI_PRODUCT_COPY_TOOL_NAME,\n description: 'Propose the storefront copy for one product. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['description', 'seoTitle', 'seoDescription', 'tags', 'categoryIds', 'optionNames'],\n properties: {\n description: string(\n `The product description shoppers read, in short paragraphs, at most ${AI_PRODUCT_DESCRIPTION_MAX_CHARS} characters.`,\n ),\n seoTitle: string(`The search result headline, published as written, at most ${SEO_TITLE_MAX} characters.`),\n seoDescription: string(`The search result summary, at most ${SEO_DESCRIPTION_MAX} characters.`),\n tags: strings(`At most ${AI_PRODUCT_TAGS_MAX} tags, each at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`),\n categoryIds: strings(\n `At most ${AI_PRODUCT_CATEGORIES_MAX} category ids from the list you were given; empty when none fits.`,\n ),\n optionNames: strings(\n `One name per option the product has, in order, each at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters; empty when it has none.`,\n ),\n },\n },\n}\n\nexport interface AiProductCopyCheckContext {\n /** The ids of the site's categories the request listed. */\n categoryIds: readonly string[]\n /** How many options the product has. */\n optionCount: number\n /** The merchant's own words about the product: its name, text and tags. */\n merchantWords: string\n}\n\n/**\n * A product copy answer held to the editor: every field written and within\n * its length, plain text, the categories the request listed, one distinct\n * name per option, and no claim or price the copy may not make.\n */\nexport function checkAiProductCopy(\n answer: Record<string, unknown>,\n context: AiProductCopyCheckContext,\n): AiGenerationCheckResult<AiProductCopy> {\n const findings: Findings = { violations: [], offending: {} }\n const description = checkCopyText(findings, 'description', answer['description'], {\n label: 'The description',\n max: AI_PRODUCT_DESCRIPTION_MAX_CHARS,\n multiline: true,\n })\n const seoTitle = checkCopyText(findings, 'seoTitle', answer['seoTitle'], {\n label: 'The search title',\n max: SEO_TITLE_MAX,\n brackets: false,\n })\n const seoDescription = checkCopyText(findings, 'seoDescription', answer['seoDescription'], {\n label: 'The search description',\n max: SEO_DESCRIPTION_MAX,\n brackets: false,\n })\n const tags = checkTags(findings, 'tags', answer['tags'])\n\n const allowed = new Set(context.categoryIds)\n const categoryIds: string[] = []\n const rawCategories = answer['categoryIds']\n if (!Array.isArray(rawCategories)) {\n fail(findings, 'categoryIds', 'type', 'Categories are a list of ids.', rawCategories)\n } else {\n for (const entry of rawCategories) {\n const id = line(entry)\n if (!id || categoryIds.includes(id)) continue\n if (!allowed.has(id)) {\n fail(findings, 'categoryIds', 'category-unknown', 'Use only the category ids you were given.', rawCategories)\n continue\n }\n categoryIds.push(id)\n }\n if (categoryIds.length > AI_PRODUCT_CATEGORIES_MAX) {\n fail(findings, 'categoryIds', 'too-many', `A product goes in at most ${AI_PRODUCT_CATEGORIES_MAX} categories.`, rawCategories)\n }\n }\n\n const optionNames: string[] = []\n const rawOptions = answer['optionNames']\n if (!Array.isArray(rawOptions)) {\n fail(findings, 'optionNames', 'type', 'Option names are a list.', rawOptions)\n } else {\n if (rawOptions.length !== context.optionCount) {\n fail(\n findings,\n 'optionNames',\n 'option-count',\n `The product has ${context.optionCount} ${context.optionCount === 1 ? 'option' : 'options'}; give one name for each.`,\n rawOptions,\n )\n }\n rawOptions.slice(0, context.optionCount).forEach((entry, index) => {\n const name = line(entry)\n if (!name || name.length > AI_PRODUCT_OPTION_NAME_MAX_CHARS || BRACKET.test(name) || EMOJI.test(name)) {\n fail(findings, `optionNames[${index}]`, 'option-name', `An option name is a plain word or phrase of at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`, entry)\n }\n optionNames.push(name)\n })\n if (new Set(optionNames.map((name) => name.toLowerCase())).size !== optionNames.length) {\n fail(findings, 'optionNames', 'option-duplicate', 'Each option needs its own name.', rawOptions)\n }\n }\n\n const samples: AiStorefrontCopySample[] = [\n { at: 'description', text: description },\n { at: 'seoTitle', text: seoTitle },\n { at: 'seoDescription', text: seoDescription },\n ...tags.map((tag, index) => ({ at: `tags[${index}]`, text: tag })),\n ]\n findings.violations.push(\n ...claimViolations(samples, context.merchantWords),\n ...detectOffVoiceCopy(samples, null),\n )\n const value = findings.violations.length\n ? null\n : { description, seoTitle, seoDescription, tags, categoryIds, optionNames }\n return { value, violations: findings.violations, offending: findings.offending }\n}\n\n/* ------------------------------------------------------------------------ *\n * A store's first products\n * ------------------------------------------------------------------------ */\n\nexport const AI_CATALOG_TOOL: AiTool = {\n name: AI_CATALOG_TOOL_NAME,\n description: 'Propose the products of a new store, each a draft its owner completes. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['products'],\n properties: {\n products: {\n type: 'array',\n description: `Between ${AI_CATALOG_PRODUCTS.min} and ${AI_CATALOG_PRODUCTS.max} products.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type', 'description', 'tags', 'options', 'seoTitle', 'seoDescription', 'photo', 'price'],\n properties: {\n name: string(`Distinct within the list, at most ${AI_PRODUCT_NAME_MAX_CHARS} characters.`),\n type: { type: 'string', enum: [...AI_PRODUCT_TYPES] },\n description: string(`In short paragraphs, at most ${AI_CATALOG_DESCRIPTION_MAX_CHARS} characters.`),\n tags: strings(`At most ${AI_CATALOG_TAGS_MAX} tags, each at most ${AI_PRODUCT_TAG_MAX_CHARS} characters.`),\n options: {\n type: 'array',\n description: `The choices a shopper makes, at most ${AI_CATALOG_OPTIONS_MAX}; empty for a product with none.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'values'],\n properties: {\n name: string(`At most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`),\n values: strings(\n `At most ${AI_CATALOG_OPTION_VALUES_MAX} distinct values, each at most ${AI_CATALOG_OPTION_VALUE_MAX_CHARS} characters.`,\n ),\n },\n },\n },\n seoTitle: string(`At most ${SEO_TITLE_MAX} characters.`),\n seoDescription: string(`At most ${SEO_DESCRIPTION_MAX} characters.`),\n photo: string(`What the product photo should show, in one sentence of at most ${AI_CATALOG_PHOTO_MAX_CHARS} characters.`),\n price: {\n type: 'number',\n description: `Its suggested price in US dollars, from ${AI_CATALOG_PRICE_USD.min} to ${AI_CATALOG_PRICE_USD.max}.`,\n },\n },\n },\n },\n },\n },\n}\n\n/** A catalog answer held to the products editor and the storefront copy rules. */\nexport function checkAiCatalog(\n answer: Record<string, unknown>,\n context: { merchantWords: string },\n): AiGenerationCheckResult<AiCatalogProduct[]> {\n const findings: Findings = { violations: [], offending: {} }\n const raw = answer['products']\n if (!Array.isArray(raw)) {\n fail(findings, 'products', 'type', 'The products are a list.', raw)\n return { value: null, violations: findings.violations, offending: findings.offending }\n }\n if (raw.length < AI_CATALOG_PRODUCTS.min || raw.length > AI_CATALOG_PRODUCTS.max) {\n fail(findings, 'products', 'product-count', `Propose between ${AI_CATALOG_PRODUCTS.min} and ${AI_CATALOG_PRODUCTS.max} products.`, `${raw.length} products`)\n }\n const names = new Set<string>()\n const products: AiCatalogProduct[] = []\n const samples: AiStorefrontCopySample[] = []\n raw.slice(0, AI_CATALOG_PRODUCTS.max).forEach((entry, index) => {\n const at = `products[${index}]`\n if (!isRecord(entry)) {\n fail(findings, at, 'type', 'Each product is an object.', entry)\n return\n }\n const name = checkCopyText(findings, `${at}.name`, entry['name'], {\n label: 'A product name',\n max: AI_PRODUCT_NAME_MAX_CHARS,\n brackets: false,\n })\n if (name && names.has(name.toLowerCase())) {\n fail(findings, `${at}.name`, 'name-duplicate', 'Each product needs its own name.', entry['name'])\n }\n names.add(name.toLowerCase())\n const type = entry['type']\n if (!(AI_PRODUCT_TYPES as readonly unknown[]).includes(type)) {\n fail(findings, `${at}.type`, 'type', 'A product is physical, digital or a service.', type)\n }\n const description = checkCopyText(findings, `${at}.description`, entry['description'], {\n label: 'A product description',\n max: AI_CATALOG_DESCRIPTION_MAX_CHARS,\n multiline: true,\n })\n const tags = checkTags(findings, `${at}.tags`, entry['tags'], AI_CATALOG_TAGS_MAX)\n const options = checkCatalogOptions(findings, `${at}.options`, entry['options'])\n const seoTitle = checkCopyText(findings, `${at}.seoTitle`, entry['seoTitle'], {\n label: 'A search title',\n max: SEO_TITLE_MAX,\n brackets: false,\n })\n const seoDescription = checkCopyText(findings, `${at}.seoDescription`, entry['seoDescription'], {\n label: 'A search description',\n max: SEO_DESCRIPTION_MAX,\n brackets: false,\n })\n const photo = checkCopyText(findings, `${at}.photo`, entry['photo'], {\n label: 'What the photo shows',\n max: AI_CATALOG_PHOTO_MAX_CHARS,\n brackets: false,\n })\n samples.push(\n { at: `${at}.name`, text: name },\n { at: `${at}.description`, text: description },\n { at: `${at}.seoTitle`, text: seoTitle },\n { at: `${at}.seoDescription`, text: seoDescription },\n ...tags.map((tag, tagIndex) => ({ at: `${at}.tags[${tagIndex}]`, text: tag })),\n )\n products.push({\n name,\n type: type as AiProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n photo,\n // Kept by code, never re-asked (Zach, 2026-10-12): a price that is\n // missing or out of range is `null`, and the store's default stands in.\n price: aiCatalogPrice(entry['price']),\n })\n })\n findings.violations.push(...claimViolations(samples, context.merchantWords), ...detectOffVoiceCopy(samples, null))\n return {\n value: findings.violations.length ? null : products,\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n\nfunction checkCatalogOptions(findings: Findings, at: string, raw: unknown): AiProductOption[] {\n if (!Array.isArray(raw)) {\n fail(findings, at, 'type', 'Options are a list.', raw)\n return []\n }\n if (raw.length > AI_CATALOG_OPTIONS_MAX) {\n fail(findings, at, 'too-many', `A proposed product has at most ${AI_CATALOG_OPTIONS_MAX} options.`, raw)\n }\n const options: AiProductOption[] = []\n raw.slice(0, AI_CATALOG_OPTIONS_MAX).forEach((entry, index) => {\n const here = `${at}[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = line(record['name'])\n if (!name || name.length > AI_PRODUCT_OPTION_NAME_MAX_CHARS || BRACKET.test(name)) {\n fail(findings, `${here}.name`, 'option-name', `An option name is a plain word or phrase of at most ${AI_PRODUCT_OPTION_NAME_MAX_CHARS} characters.`, record['name'])\n }\n const values = Array.isArray(record['values']) ? record['values'].map(line).filter(Boolean) : []\n if (!values.length || values.length > AI_CATALOG_OPTION_VALUES_MAX) {\n fail(findings, `${here}.values`, 'too-many', `An option has between 1 and ${AI_CATALOG_OPTION_VALUES_MAX} values.`, record['values'])\n }\n if (values.some((value) => value.length > AI_CATALOG_OPTION_VALUE_MAX_CHARS || BRACKET.test(value))) {\n fail(findings, `${here}.values`, 'option-values', `An option value is a plain word or phrase of at most ${AI_CATALOG_OPTION_VALUE_MAX_CHARS} characters.`, record['values'])\n }\n if (new Set(values.map((value) => value.toLowerCase())).size !== values.length) {\n fail(findings, `${here}.values`, 'option-values', 'An option’s values are all different.', record['values'])\n }\n options.push({ name, values: values.slice(0, AI_CATALOG_OPTION_VALUES_MAX) })\n })\n if (new Set(options.map((option) => option.name.toLowerCase())).size !== options.length) {\n fail(findings, at, 'option-duplicate', 'Each option needs its own name.', raw)\n }\n return options\n}\n\n/* ------------------------------------------------------------------------ *\n * Categories and discounts\n * ------------------------------------------------------------------------ */\n\nexport const AI_CATEGORIES_TOOL: AiTool = {\n name: AI_CATEGORIES_TOOL_NAME,\n description: 'Propose store categories and a first set of discounts, each created only if the owner accepts it. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['categories', 'discounts'],\n properties: {\n categories: {\n type: 'array',\n description: `At most ${AI_CATEGORIES_MAX} categories; empty when the brief gives no reason for any.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'why'],\n properties: {\n name: string(`At most ${AI_CATEGORY_NAME_MAX_CHARS} characters.`),\n why: string(`One sentence of at most ${AI_PROPOSAL_WHY_MAX_CHARS} characters.`),\n },\n },\n },\n discounts: {\n type: 'array',\n description: `At most ${AI_DISCOUNTS_MAX} discounts; empty when the brief gives no reason for any.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'code', 'kind', 'value', 'minimumOrderUsd', 'why'],\n properties: {\n name: string(`At most ${AI_CATEGORY_NAME_MAX_CHARS} characters.`),\n code: string('The code a shopper types: 3 to 20 capital letters, digits, dashes or underscores; empty for a discount that applies on its own.'),\n kind: { type: 'string', enum: ['percent', 'fixed', 'free_shipping'] },\n value: {\n type: 'number',\n description: `percent: the percentage, 1 to ${AI_DISCOUNT_MAX_PERCENT}; fixed: dollars off, at most ${AI_DISCOUNT_MAX_FIXED_USD}; free_shipping: 0.`,\n },\n minimumOrderUsd: { type: 'number', description: 'The smallest order it applies to, in dollars; 0 for any order.' },\n why: string(`One sentence of at most ${AI_PROPOSAL_WHY_MAX_CHARS} characters.`),\n },\n },\n },\n },\n },\n}\n\n/** A categories answer held to the catalog and discount editors and the storefront copy rules. */\nexport function checkAiCategories(\n answer: Record<string, unknown>,\n context: { existingCategoryNames: readonly string[]; merchantWords: string },\n): AiGenerationCheckResult<Pick<AiCategoriesProposal, 'categories' | 'discounts'>> {\n const findings: Findings = { violations: [], offending: {} }\n const existing = new Set(context.existingCategoryNames.map((name) => line(name).toLowerCase()))\n const samples: AiStorefrontCopySample[] = []\n\n const categories: AiCategoriesProposal['categories'] = []\n const rawCategories = answer['categories']\n if (!Array.isArray(rawCategories)) {\n fail(findings, 'categories', 'type', 'Categories are a list.', rawCategories)\n } else {\n if (rawCategories.length > AI_CATEGORIES_MAX) {\n fail(findings, 'categories', 'too-many', `Propose at most ${AI_CATEGORIES_MAX} categories.`, `${rawCategories.length} categories`)\n }\n const seen = new Set<string>()\n rawCategories.slice(0, AI_CATEGORIES_MAX).forEach((entry, index) => {\n const at = `categories[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = checkCopyText(findings, `${at}.name`, record['name'], {\n label: 'A category name',\n max: AI_CATEGORY_NAME_MAX_CHARS,\n brackets: false,\n })\n const key = name.toLowerCase()\n if (name && existing.has(key)) {\n fail(findings, `${at}.name`, 'category-exists', `The store already has a category named ${name}.`, record['name'])\n } else if (name && seen.has(key)) {\n fail(findings, `${at}.name`, 'category-duplicate', 'Each category needs its own name.', record['name'])\n }\n seen.add(key)\n const why = checkCopyText(findings, `${at}.why`, record['why'], {\n label: 'Why a category helps',\n max: AI_PROPOSAL_WHY_MAX_CHARS,\n brackets: false,\n })\n samples.push({ at: `${at}.name`, text: name }, { at: `${at}.why`, text: why })\n categories.push({ name, why })\n })\n }\n\n const discounts: AiProposedDiscount[] = []\n const rawDiscounts = answer['discounts']\n if (!Array.isArray(rawDiscounts)) {\n fail(findings, 'discounts', 'type', 'Discounts are a list.', rawDiscounts)\n } else {\n if (rawDiscounts.length > AI_DISCOUNTS_MAX) {\n fail(findings, 'discounts', 'too-many', `Propose at most ${AI_DISCOUNTS_MAX} discounts.`, `${rawDiscounts.length} discounts`)\n }\n const codes = new Set<string>()\n rawDiscounts.slice(0, AI_DISCOUNTS_MAX).forEach((entry, index) => {\n const at = `discounts[${index}]`\n const record = isRecord(entry) ? entry : {}\n const name = checkCopyText(findings, `${at}.name`, record['name'], {\n label: 'A discount name',\n max: AI_CATEGORY_NAME_MAX_CHARS,\n brackets: false,\n })\n const typedCode = line(record['code']).toUpperCase()\n if (typedCode && !AI_DISCOUNT_CODE.test(typedCode)) {\n fail(findings, `${at}.code`, 'code', 'A code is 3 to 20 capital letters, digits, dashes or underscores.', record['code'])\n } else if (typedCode && codes.has(typedCode)) {\n fail(findings, `${at}.code`, 'code-duplicate', 'Each discount needs its own code.', record['code'])\n }\n if (typedCode) codes.add(typedCode)\n const kind = record['kind']\n const value = Number(record['value'])\n const minimum = Number(record['minimumOrderUsd'])\n let valuePct: number | null = null\n let valueCents: number | null = null\n if (kind === 'percent') {\n if (!Number.isInteger(value) || value < 1 || value > AI_DISCOUNT_MAX_PERCENT) {\n fail(findings, `${at}.value`, 'discount-value', `A percentage is a whole number from 1 to ${AI_DISCOUNT_MAX_PERCENT}.`, record['value'])\n }\n valuePct = value\n } else if (kind === 'fixed') {\n if (!Number.isFinite(value) || value <= 0 || value > AI_DISCOUNT_MAX_FIXED_USD) {\n fail(findings, `${at}.value`, 'discount-value', `An amount off is more than 0 and at most ${AI_DISCOUNT_MAX_FIXED_USD} dollars.`, record['value'])\n }\n valueCents = Math.round(value * 100)\n } else if (kind !== 'free_shipping') {\n fail(findings, `${at}.kind`, 'type', 'A discount is a percentage, an amount off or free shipping.', kind)\n }\n if (!Number.isFinite(minimum) || minimum < 0 || minimum > 10_000) {\n fail(findings, `${at}.minimumOrderUsd`, 'discount-minimum', 'A minimum order is between 0 and 10,000 dollars.', record['minimumOrderUsd'])\n }\n const why = checkCopyText(findings, `${at}.why`, record['why'], {\n label: 'Why a discount helps',\n max: AI_PROPOSAL_WHY_MAX_CHARS,\n brackets: false,\n })\n // The discount's own amount is stated in its fields, so only its name\n // and reason are read for a claim: \"15% off\" is the discount, not a price.\n samples.push({ at: `${at}.name`, text: name }, { at: `${at}.why`, text: why })\n discounts.push({\n name,\n code: typedCode || null,\n kind: kind as AiDiscountKind,\n valuePct,\n valueCents,\n minSubtotalCents: minimum > 0 ? Math.round(minimum * 100) : null,\n why,\n })\n })\n }\n\n if (!categories.length && !discounts.length && !findings.violations.length) {\n fail(findings, 'categories', 'empty', 'Propose at least one category or one discount.', answer)\n }\n findings.violations.push(\n ...storefrontClaimViolations(findStorefrontClaims(samples, context.merchantWords)),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { categories, discounts },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["SEO_LISTING_FIELDS","AI_CATALOG_DESCRIPTION_MAX_CHARS","AI_CATALOG_OPTION_VALUE_MAX_CHARS","AI_CATALOG_OPTION_VALUES_MAX","AI_CATALOG_OPTIONS_MAX","AI_CATALOG_PHOTO_MAX_CHARS","AI_CATALOG_PRICE_USD","AI_CATALOG_PRODUCTS","AI_CATALOG_TAGS_MAX","AI_CATEGORIES_MAX","AI_CATEGORY_NAME_MAX_CHARS","AI_DISCOUNT_CODE","AI_DISCOUNT_MAX_FIXED_USD","AI_DISCOUNT_MAX_PERCENT","AI_DISCOUNTS_MAX","AI_PRODUCT_CATEGORIES_MAX","AI_PRODUCT_DESCRIPTION_MAX_CHARS","AI_PRODUCT_NAME_MAX_CHARS","AI_PRODUCT_OPTION_NAME_MAX_CHARS","AI_PRODUCT_TAG_MAX_CHARS","AI_PRODUCT_TAGS_MAX","AI_PRODUCT_TYPES","AI_PROPOSAL_WHY_MAX_CHARS","aiCatalogPrice","findInventedPrices","findStorefrontClaims","storefrontClaimViolations","detectOffVoiceCopy","AI_PRODUCT_COPY_TOOL_NAME","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL_NAME","SEO_TITLE_MAX","title","maxLength","SEO_DESCRIPTION_MAX","description","string","type","strings","items","isRecord","value","Array","isArray","line","replace","trim","paragraphs","split","map","part","join","MARKUP","MARKDOWN","EMOJI","BRACKET","URL","fail","findings","at","code","message","violations","push","rule","paths","offending","checkCopyText","raw","bounds","label","multiline","length","max","test","brackets","checkTags","seen","Set","tags","forEach","entry","index","tag","key","toLowerCase","has","add","claimViolations","samples","merchantWords","AI_PRODUCT_COPY_TOOL","name","strict","inputSchema","additionalProperties","required","properties","seoTitle","seoDescription","categoryIds","optionNames","checkAiProductCopy","answer","context","allowed","rawCategories","id","includes","rawOptions","optionCount","slice","size","text","AI_CATALOG_TOOL","products","min","enum","options","values","photo","price","checkAiCatalog","names","checkCatalogOptions","tagIndex","here","record","filter","Boolean","some","option","AI_CATEGORIES_TOOL","categories","why","discounts","kind","minimumOrderUsd","checkAiCategories","existing","existingCategoryNames","rawDiscounts","codes","typedCode","toUpperCase","Number","minimum","valuePct","valueCents","isInteger","isFinite","Math","round","minSubtotalCents"],"mappings":"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"}
@@ -22,9 +22,14 @@ export declare const AI_CREDITS_QUOTA_KEY = "assistCredits";
22
22
  * check the usage-alerts sweep runs beside its own.
23
23
  *
24
24
  * Measured in CREDITS — the unit the customer was sold and the meter shows —
25
- * from this plugin's line of the month's spend (the billed `estCostUsd`)
26
- * through the one conversion, so this alert and the Billing meter cannot
27
- * disagree about how far into the band the org is. A plan with no band
25
+ * from the workspace's month (`orgs/{orgId}/assistUsage/{month}`) NET OF
26
+ * CREDITS GIVEN BACK (`assistSpendAfterReturnsUsd`, AGL-3595), through the one
27
+ * conversion, so this alert, the Billing meter, the Assist usage strip and
28
+ * the reservation cannot disagree about how far into the band the org is.
29
+ * The month's spend line is gross — `estCostUsd` alone — and read off it, a
30
+ * Free workspace whose test resets and refunded failures had handed 252 of
31
+ * 442 credits back was told it had used "442 — more than the 300 your plan
32
+ * includes". A plan with no band
28
33
  * resolves `null` and reads as a limit of 0, which the sweep skips the way it
29
34
  * skips Free's zero-quota dimensions.
30
35
  *
@@ -35,3 +40,8 @@ export declare const AI_CREDITS_QUOTA_KEY = "assistCredits";
35
40
  * can never incur would be the surprise bill in reverse.
36
41
  */
37
42
  export declare function aiQuotaChecks(context: UsageQuotaContext): Promise<readonly UsageQuotaCheck[]>;
43
+ /**
44
+ * The checks, given the workspace's month spend in billed USD, already net
45
+ * of give-backs.
46
+ */
47
+ export declare function aiQuotaChecksFrom(context: Pick<UsageQuotaContext, 'org' | 'month'>, spentUsd: number): readonly UsageQuotaCheck[];
@@ -14,16 +14,24 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { ASSIST_HARD_CAP_CONTROL_LABEL, ASSIST_HARD_CAP_CONTROL_LOCATION, assistBandRefuses, assistCreditsFromUsd, resolveAssistCreditBudget, resolveAssistHardCap, resolveAssistOverageRateUsdPer1k } from "./assist-credits.js";
17
- import { AI_ASSIST_SPEND_LINE_ID } from "../usage-axes.js";
17
+ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "./assist-credit-returns.js";
20
+ import { aiFreeCreditsResetLabel, aiFreeCreditsResetOn } from "../model/ai-site-job.js";
18
21
  /** The guard key the credits band is warned under. */ export const AI_CREDITS_QUOTA_KEY = 'assistCredits';
19
22
  /**
20
23
  * The AI credits band the WORKSPACE is warned about (AGL-2898), as a quota
21
24
  * check the usage-alerts sweep runs beside its own.
22
25
  *
23
26
  * Measured in CREDITS — the unit the customer was sold and the meter shows —
24
- * from this plugin's line of the month's spend (the billed `estCostUsd`)
25
- * through the one conversion, so this alert and the Billing meter cannot
26
- * disagree about how far into the band the org is. A plan with no band
27
+ * from the workspace's month (`orgs/{orgId}/assistUsage/{month}`) NET OF
28
+ * CREDITS GIVEN BACK (`assistSpendAfterReturnsUsd`, AGL-3595), through the one
29
+ * conversion, so this alert, the Billing meter, the Assist usage strip and
30
+ * the reservation cannot disagree about how far into the band the org is.
31
+ * The month's spend line is gross — `estCostUsd` alone — and read off it, a
32
+ * Free workspace whose test resets and refunded failures had handed 252 of
33
+ * 442 credits back was told it had used "442 — more than the 300 your plan
34
+ * includes". A plan with no band
27
35
  * resolves `null` and reads as a limit of 0, which the sweep skips the way it
28
36
  * skips Free's zero-quota dimensions.
29
37
  *
@@ -33,11 +41,41 @@ import { AI_ASSIST_SPEND_LINE_ID } from "../usage-axes.js";
33
41
  * ever billed — and telling a Free org to "set a monthly cap" on a charge it
34
42
  * can never incur would be the surprise bill in reverse.
35
43
  */ export async function aiQuotaChecks(context) {
36
- var _ref;
37
- var _context_spend_lines_find;
44
+ const monthDoc = await firebaseAdmin.app().firestore().collection('orgs').doc(context.orgId).collection('assistUsage').doc(context.month).get();
45
+ // Net of credits given back (AGL-3595) — the figure the gate admits on.
46
+ const spentUsd = assistSpendAfterReturnsUsd(monthDoc.get('estCostUsd'), monthDoc.get(ASSIST_RETURNED_USD_FIELD));
47
+ return aiQuotaChecksFrom(context, spentUsd);
48
+ }
49
+ /**
50
+ * The checks, given the workspace's month spend in billed USD, already net
51
+ * of give-backs.
52
+ */ export function aiQuotaChecksFrom(context, spentUsd) {
38
53
  const org = context.org;
39
54
  const bandCredits = resolveAssistCreditBudget(org);
40
- const spentUsd = (_ref = (_context_spend_lines_find = context.spend.lines.find((line)=>line.id === AI_ASSIST_SPEND_LINE_ID)) == null ? void 0 : _context_spend_lines_find.usd) != null ? _ref : 0;
55
+ const used = assistCreditsFromUsd(spentUsd);
56
+ // FREE IS AN ALLOWANCE, NOT A BAND SOLD PAST. Nothing past it is ever
57
+ // billed, so the notice says the credits are used for the month, when they
58
+ // renew, and that an upgrade adds more — never "more than your plan
59
+ // includes", which reads like the start of a charge.
60
+ if (resolveEffectivePlan(org) === 'free') {
61
+ const renews = aiFreeCreditsResetLabel(aiFreeCreditsResetOn(new Date(`${context.month}-01T00:00:00.000Z`)));
62
+ return [
63
+ {
64
+ key: AI_CREDITS_QUOTA_KEY,
65
+ label: 'free AI credits',
66
+ noun: 'free AI credits',
67
+ used,
68
+ limit: bandCredits != null ? bandCredits : 0,
69
+ cadence: 'monthly',
70
+ outcome: 'stops',
71
+ reachedTitle: "You've used your free AI credits for this month",
72
+ reachedLead: 'has used its free AI credits for this month.',
73
+ reached: `They renew on ${renews}. Upgrade for more.`,
74
+ reachedLinksUpgrade: true,
75
+ approach: 'Nothing is ever charged on Free. When they are used, AI assist ' + `pauses until your credits renew on ${renews}, or upgrade for more.`
76
+ }
77
+ ];
78
+ }
41
79
  // Whether crossing the band produces an invoice line or a stop. The same
42
80
  // predicate the reservation refuses on, so the words and the behavior
43
81
  // cannot drift: a band that refuses — the org's own switch, or a plan with
@@ -55,7 +93,7 @@ import { AI_ASSIST_SPEND_LINE_ID } from "../usage-axes.js";
55
93
  key: AI_CREDITS_QUOTA_KEY,
56
94
  label: 'AI assist credits',
57
95
  noun: 'AI assist credits',
58
- used: assistCreditsFromUsd(spentUsd),
96
+ used,
59
97
  limit: bandCredits != null ? bandCredits : 0,
60
98
  cadence: 'monthly',
61
99
  outcome: bandIsWall ? 'stops' : 'bills',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-quota-checks.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 ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n assistBandRefuses,\n assistCreditsFromUsd,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageRateUsdPer1k,\n} from './assist-credits'\nimport type {\n UsageQuotaCheck,\n UsageQuotaContext,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { AI_ASSIST_SPEND_LINE_ID } from '../usage-axes'\n\n/** The guard key the credits band is warned under. */\nexport const AI_CREDITS_QUOTA_KEY = 'assistCredits'\n\n/**\n * The AI credits band the WORKSPACE is warned about (AGL-2898), as a quota\n * check the usage-alerts sweep runs beside its own.\n *\n * Measured in CREDITS — the unit the customer was sold and the meter shows —\n * from this plugin's line of the month's spend (the billed `estCostUsd`)\n * through the one conversion, so this alert and the Billing meter cannot\n * disagree about how far into the band the org is. A plan with no band\n * resolves `null` and reads as a limit of 0, which the sweep skips the way it\n * skips Free's zero-quota dimensions.\n *\n * Approaching the band is the sweep's generic warning; REACHING it says what\n * happens next, which differs by plan and by the org's own controls — metered\n * at a rate unless a stop is set, or stopped until next month with nothing\n * ever billed — and telling a Free org to \"set a monthly cap\" on a charge it\n * can never incur would be the surprise bill in reverse.\n */\nexport async function aiQuotaChecks(\n context: UsageQuotaContext,\n): Promise<readonly UsageQuotaCheck[]> {\n const org = context.org as never\n const bandCredits = resolveAssistCreditBudget(org)\n const spentUsd =\n context.spend.lines.find((line) => line.id === AI_ASSIST_SPEND_LINE_ID)\n ?.usd ?? 0\n // Whether crossing the band produces an invoice line or a stop. The same\n // predicate the reservation refuses on, so the words and the behavior\n // cannot drift: a band that refuses — the org's own switch, or a plan with\n // no rate — is a wall, and everything else is sold past.\n const bandIsWall = assistBandRefuses(org)\n // Through the same resolver `assistBandRefuses` asks, so the alert's rate\n // and its wall/sold verdict cannot disagree about one org. Read straight off\n // `PLAN_PRICING` they did: Starter's listed rate is null and the add-on's is\n // added by the resolver, so a Starter workspace with the add-on was told\n // \"sold past the band\" at \"$0.00 per 1,000\" in the same sentence (AGL-3014).\n const rateUsdPer1k = resolveAssistOverageRateUsdPer1k(org)\n const rate = `$${(rateUsdPer1k ?? 0).toFixed(2)} per 1,000`\n return [\n {\n key: AI_CREDITS_QUOTA_KEY,\n label: 'AI assist credits',\n noun: 'AI assist credits',\n used: assistCreditsFromUsd(spentUsd),\n limit: bandCredits ?? 0,\n cadence: 'monthly',\n outcome: bandIsWall ? 'stops' : 'bills',\n reachedTitle: bandIsWall\n ? \"You've used your included AI assist credits\"\n : \"You've used your included AI assist credits — extra credits \" +\n 'are now billed',\n reached: bandIsWall\n ? 'AI assist stops here until next month — nothing past the band ' +\n 'is ever billed. Upgrade in Billing to add credits' +\n (resolveAssistHardCap(org) && rateUsdPer1k !== null\n ? `, or turn off \"${ASSIST_HARD_CAP_CONTROL_LABEL}\" ` +\n `under ${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep ` +\n `going at your plan’s rate.`\n : '.')\n : 'The assistant keeps answering, and the extra credits are ' +\n `metered on your monthly invoice at ${rate} — unless you set a ` +\n `stop under ${ASSIST_HARD_CAP_CONTROL_LOCATION}: stop at the ` +\n 'included band, or stop once this month’s overage reaches a ' +\n 'figure you choose.',\n approach: bandIsWall\n ? 'Nothing changes and nothing is charged — at the included band, ' +\n 'AI assist stops until next month, and nothing past it is ever ' +\n 'billed.'\n : 'Nothing is charged yet — past the included band, extra credits ' +\n `are metered on your monthly invoice at ${rate} unless you set a ` +\n `stop under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`,\n },\n ]\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","assistBandRefuses","assistCreditsFromUsd","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageRateUsdPer1k","AI_ASSIST_SPEND_LINE_ID","AI_CREDITS_QUOTA_KEY","aiQuotaChecks","context","org","bandCredits","spentUsd","spend","lines","find","line","id","usd","bandIsWall","rateUsdPer1k","rate","toFixed","key","label","noun","used","limit","cadence","outcome","reachedTitle","reached","approach"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,6BAA6B,EAC7BC,gCAAgC,EAChCC,iBAAiB,EACjBC,oBAAoB,EACpBC,yBAAyB,EACzBC,oBAAoB,EACpBC,gCAAgC,QAC3B,sBAAkB;AAKzB,SAASC,uBAAuB,QAAQ,mBAAe;AAEvD,oDAAoD,GACpD,OAAO,MAAMC,uBAAuB,gBAAe;AAEnD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeC,cACpBC,OAA0B;;QAKxBA;IAHF,MAAMC,MAAMD,QAAQC,GAAG;IACvB,MAAMC,cAAcR,0BAA0BO;IAC9C,MAAME,oBACJH,4BAAAA,QAAQI,KAAK,CAACC,KAAK,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,EAAE,KAAKX,6CAA/CG,0BACIS,GAAG,mBAAI;IACb,yEAAyE;IACzE,sEAAsE;IACtE,2EAA2E;IAC3E,yDAAyD;IACzD,MAAMC,aAAalB,kBAAkBS;IACrC,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,6EAA6E;IAC7E,MAAMU,eAAef,iCAAiCK;IACtD,MAAMW,OAAO,CAAC,CAAC,EAAE,CAACD,uBAAAA,eAAgB,GAAGE,OAAO,CAAC,GAAG,UAAU,CAAC;IAC3D,OAAO;QACL;YACEC,KAAKhB;YACLiB,OAAO;YACPC,MAAM;YACNC,MAAMxB,qBAAqBU;YAC3Be,KAAK,EAAEhB,sBAAAA,cAAe;YACtBiB,SAAS;YACTC,SAASV,aAAa,UAAU;YAChCW,cAAcX,aACV,gDACA,iEACA;YACJY,SAASZ,aACL,mEACA,sDACCf,CAAAA,qBAAqBM,QAAQU,iBAAiB,OAC3C,CAAC,eAAe,EAAErB,8BAA8B,EAAE,CAAC,GACnD,CAAC,MAAM,EAAEC,iCAAiC,SAAS,CAAC,GACpD,CAAC,0BAA0B,CAAC,GAC5B,GAAE,IACN,8DACA,CAAC,mCAAmC,EAAEqB,KAAK,oBAAoB,CAAC,GAChE,CAAC,WAAW,EAAErB,iCAAiC,cAAc,CAAC,GAC9D,gEACA;YACJgC,UAAUb,aACN,oEACA,mEACA,YACA,oEACA,CAAC,uCAAuC,EAAEE,KAAK,kBAAkB,CAAC,GAClE,CAAC,WAAW,EAAErB,iCAAiC,CAAC,CAAC;QACvD;KACD;AACH"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-quota-checks.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 ASSIST_HARD_CAP_CONTROL_LABEL,\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n assistBandRefuses,\n assistCreditsFromUsd,\n resolveAssistCreditBudget,\n resolveAssistHardCap,\n resolveAssistOverageRateUsdPer1k,\n} from './assist-credits'\nimport type {\n UsageQuotaCheck,\n UsageQuotaContext,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from './assist-credit-returns'\nimport {\n aiFreeCreditsResetLabel,\n aiFreeCreditsResetOn,\n} from '../model/ai-site-job'\n\n/** The guard key the credits band is warned under. */\nexport const AI_CREDITS_QUOTA_KEY = 'assistCredits'\n\n/**\n * The AI credits band the WORKSPACE is warned about (AGL-2898), as a quota\n * check the usage-alerts sweep runs beside its own.\n *\n * Measured in CREDITS — the unit the customer was sold and the meter shows —\n * from the workspace's month (`orgs/{orgId}/assistUsage/{month}`) NET OF\n * CREDITS GIVEN BACK (`assistSpendAfterReturnsUsd`, AGL-3595), through the one\n * conversion, so this alert, the Billing meter, the Assist usage strip and\n * the reservation cannot disagree about how far into the band the org is.\n * The month's spend line is gross — `estCostUsd` alone — and read off it, a\n * Free workspace whose test resets and refunded failures had handed 252 of\n * 442 credits back was told it had used \"442 — more than the 300 your plan\n * includes\". A plan with no band\n * resolves `null` and reads as a limit of 0, which the sweep skips the way it\n * skips Free's zero-quota dimensions.\n *\n * Approaching the band is the sweep's generic warning; REACHING it says what\n * happens next, which differs by plan and by the org's own controls — metered\n * at a rate unless a stop is set, or stopped until next month with nothing\n * ever billed — and telling a Free org to \"set a monthly cap\" on a charge it\n * can never incur would be the surprise bill in reverse.\n */\nexport async function aiQuotaChecks(\n context: UsageQuotaContext,\n): Promise<readonly UsageQuotaCheck[]> {\n const monthDoc = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(context.orgId)\n .collection('assistUsage')\n .doc(context.month)\n .get()\n // Net of credits given back (AGL-3595) — the figure the gate admits on.\n const spentUsd = assistSpendAfterReturnsUsd(\n monthDoc.get('estCostUsd'),\n monthDoc.get(ASSIST_RETURNED_USD_FIELD),\n )\n return aiQuotaChecksFrom(context, spentUsd)\n}\n\n/**\n * The checks, given the workspace's month spend in billed USD, already net\n * of give-backs.\n */\nexport function aiQuotaChecksFrom(\n context: Pick<UsageQuotaContext, 'org' | 'month'>,\n spentUsd: number,\n): readonly UsageQuotaCheck[] {\n const org = context.org as never\n const bandCredits = resolveAssistCreditBudget(org)\n const used = assistCreditsFromUsd(spentUsd)\n // FREE IS AN ALLOWANCE, NOT A BAND SOLD PAST. Nothing past it is ever\n // billed, so the notice says the credits are used for the month, when they\n // renew, and that an upgrade adds more — never \"more than your plan\n // includes\", which reads like the start of a charge.\n if (resolveEffectivePlan(org) === 'free') {\n const renews = aiFreeCreditsResetLabel(\n aiFreeCreditsResetOn(new Date(`${context.month}-01T00:00:00.000Z`)),\n )\n return [\n {\n key: AI_CREDITS_QUOTA_KEY,\n label: 'free AI credits',\n noun: 'free AI credits',\n used,\n limit: bandCredits ?? 0,\n cadence: 'monthly',\n outcome: 'stops',\n reachedTitle: \"You've used your free AI credits for this month\",\n reachedLead: 'has used its free AI credits for this month.',\n reached: `They renew on ${renews}. Upgrade for more.`,\n reachedLinksUpgrade: true,\n approach:\n 'Nothing is ever charged on Free. When they are used, AI assist ' +\n `pauses until your credits renew on ${renews}, or upgrade for more.`,\n },\n ]\n }\n // Whether crossing the band produces an invoice line or a stop. The same\n // predicate the reservation refuses on, so the words and the behavior\n // cannot drift: a band that refuses — the org's own switch, or a plan with\n // no rate — is a wall, and everything else is sold past.\n const bandIsWall = assistBandRefuses(org)\n // Through the same resolver `assistBandRefuses` asks, so the alert's rate\n // and its wall/sold verdict cannot disagree about one org. Read straight off\n // `PLAN_PRICING` they did: Starter's listed rate is null and the add-on's is\n // added by the resolver, so a Starter workspace with the add-on was told\n // \"sold past the band\" at \"$0.00 per 1,000\" in the same sentence (AGL-3014).\n const rateUsdPer1k = resolveAssistOverageRateUsdPer1k(org)\n const rate = `$${(rateUsdPer1k ?? 0).toFixed(2)} per 1,000`\n return [\n {\n key: AI_CREDITS_QUOTA_KEY,\n label: 'AI assist credits',\n noun: 'AI assist credits',\n used,\n limit: bandCredits ?? 0,\n cadence: 'monthly',\n outcome: bandIsWall ? 'stops' : 'bills',\n reachedTitle: bandIsWall\n ? \"You've used your included AI assist credits\"\n : \"You've used your included AI assist credits — extra credits \" +\n 'are now billed',\n reached: bandIsWall\n ? 'AI assist stops here until next month — nothing past the band ' +\n 'is ever billed. Upgrade in Billing to add credits' +\n (resolveAssistHardCap(org) && rateUsdPer1k !== null\n ? `, or turn off \"${ASSIST_HARD_CAP_CONTROL_LABEL}\" ` +\n `under ${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep ` +\n `going at your plan’s rate.`\n : '.')\n : 'The assistant keeps answering, and the extra credits are ' +\n `metered on your monthly invoice at ${rate} — unless you set a ` +\n `stop under ${ASSIST_HARD_CAP_CONTROL_LOCATION}: stop at the ` +\n 'included band, or stop once this month’s overage reaches a ' +\n 'figure you choose.',\n approach: bandIsWall\n ? 'Nothing changes and nothing is charged — at the included band, ' +\n 'AI assist stops until next month, and nothing past it is ever ' +\n 'billed.'\n : 'Nothing is charged yet — past the included band, extra credits ' +\n `are metered on your monthly invoice at ${rate} unless you set a ` +\n `stop under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`,\n },\n ]\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","assistBandRefuses","assistCreditsFromUsd","resolveAssistCreditBudget","resolveAssistHardCap","resolveAssistOverageRateUsdPer1k","resolveEffectivePlan","firebaseAdmin","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","aiFreeCreditsResetLabel","aiFreeCreditsResetOn","AI_CREDITS_QUOTA_KEY","aiQuotaChecks","context","monthDoc","app","firestore","collection","doc","orgId","month","get","spentUsd","aiQuotaChecksFrom","org","bandCredits","used","renews","Date","key","label","noun","limit","cadence","outcome","reachedTitle","reachedLead","reached","reachedLinksUpgrade","approach","bandIsWall","rateUsdPer1k","rate","toFixed"],"mappings":"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"}