@aglyn/plugins-ai 1.0.0-beta.231 → 1.0.0-beta.232

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 (220) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +8 -4
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-follow.component.js +1 -0
  5. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  6. package/src/lib/components/ai-job-links.js +13 -0
  7. package/src/lib/components/ai-job-links.js.map +1 -1
  8. package/src/lib/components/ai-jobs-list-page.component.js +1 -0
  9. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  10. package/src/lib/components/ai-media-create.component.d.ts +20 -7
  11. package/src/lib/components/ai-media-create.component.js +107 -55
  12. package/src/lib/components/ai-media-create.component.js.map +1 -1
  13. package/src/lib/components/ai-seo-fields-card.component.js +1 -0
  14. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  15. package/src/lib/components/ai-site-memory-card.component.d.ts +32 -0
  16. package/src/lib/components/ai-site-memory-card.component.js +187 -0
  17. package/src/lib/components/ai-site-memory-card.component.js.map +1 -0
  18. package/src/lib/components/ai-site-start-card.component.js +80 -77
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-theme-proposal-card.component.js +4 -0
  21. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  22. package/src/lib/components/ai-usage-strip.component.js +1 -0
  23. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  24. package/src/lib/components/assist-panel.component.d.ts +10 -0
  25. package/src/lib/components/assist-panel.component.js +119 -9
  26. package/src/lib/components/assist-panel.component.js.map +1 -1
  27. package/src/lib/components/assist-signals-page.component.js +2 -0
  28. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  29. package/src/lib/components/billing-ai-allotments.component.js +3 -0
  30. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  31. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  32. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  33. package/src/lib/components/billing-assist-overage-card.component.js +2 -0
  34. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  35. package/src/lib/components/member-ai-usage-card.component.js +10 -0
  36. package/src/lib/components/member-ai-usage-card.component.js.map +1 -1
  37. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  38. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  39. package/src/lib/components/staff-org-ai-conversations.component.d.ts +23 -0
  40. package/src/lib/components/staff-org-ai-conversations.component.js +346 -0
  41. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -0
  42. package/src/lib/jobs/ai-build-capabilities.d.ts +13 -1
  43. package/src/lib/jobs/ai-build-capabilities.js +101 -7
  44. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  45. package/src/lib/jobs/ai-business-profile-prefill.d.ts +29 -0
  46. package/src/lib/jobs/ai-business-profile-prefill.js +85 -0
  47. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -0
  48. package/src/lib/jobs/ai-job-build-step.js +3 -1
  49. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-edit-step.d.ts +225 -0
  51. package/src/lib/jobs/ai-job-edit-step.js +631 -0
  52. package/src/lib/jobs/ai-job-edit-step.js.map +1 -0
  53. package/src/lib/jobs/ai-job-insight-step.js +21 -4
  54. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  55. package/src/lib/jobs/ai-job-layout-language.d.ts +8 -3
  56. package/src/lib/jobs/ai-job-layout-language.js +8 -2
  57. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  58. package/src/lib/jobs/ai-job-layout-step.js +7 -6
  59. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  60. package/src/lib/jobs/ai-job-logic-step.d.ts +20 -0
  61. package/src/lib/jobs/ai-job-logic-step.js +87 -6
  62. package/src/lib/jobs/ai-job-logic-step.js.map +1 -1
  63. package/src/lib/jobs/ai-job-page-language.d.ts +52 -7
  64. package/src/lib/jobs/ai-job-page-language.js +72 -9
  65. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  66. package/src/lib/jobs/ai-job-page-step.js +12 -2
  67. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  68. package/src/lib/jobs/ai-job-plan-step.d.ts +19 -5
  69. package/src/lib/jobs/ai-job-plan-step.js +54 -20
  70. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  71. package/src/lib/jobs/ai-job-site-content.d.ts +163 -0
  72. package/src/lib/jobs/ai-job-site-content.js +443 -0
  73. package/src/lib/jobs/ai-job-site-content.js.map +1 -0
  74. package/src/lib/jobs/ai-job-site-look.d.ts +112 -0
  75. package/src/lib/jobs/ai-job-site-look.js +322 -0
  76. package/src/lib/jobs/ai-job-site-look.js.map +1 -0
  77. package/src/lib/jobs/ai-job-site-step.d.ts +26 -5
  78. package/src/lib/jobs/ai-job-site-step.js +163 -24
  79. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  80. package/src/lib/jobs/ai-jobs-notify.d.ts +6 -1
  81. package/src/lib/jobs/ai-jobs-notify.js +15 -3
  82. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  83. package/src/lib/layout-language/ai-layout-compiler.d.ts +9 -0
  84. package/src/lib/layout-language/ai-layout-compiler.js +120 -33
  85. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  86. package/src/lib/layout-language/ai-layout-frame.js +5 -4
  87. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  88. package/src/lib/layout-language/ai-layout-language.d.ts +12 -5
  89. package/src/lib/layout-language/ai-layout-language.js +92 -9
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-pictures.d.ts +144 -0
  92. package/src/lib/layout-language/ai-layout-pictures.js +214 -0
  93. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -0
  94. package/src/lib/layout-language/ai-layout-store.d.ts +2 -0
  95. package/src/lib/layout-language/ai-layout-store.js +7 -1
  96. package/src/lib/layout-language/ai-layout-store.js.map +1 -1
  97. package/src/lib/model/ai-build-progress.js +4 -1
  98. package/src/lib/model/ai-build-progress.js.map +1 -1
  99. package/src/lib/model/ai-edit-job.d.ts +109 -0
  100. package/src/lib/model/ai-edit-job.js +157 -0
  101. package/src/lib/model/ai-edit-job.js.map +1 -0
  102. package/src/lib/model/ai-job-activity.js +1 -0
  103. package/src/lib/model/ai-job-activity.js.map +1 -1
  104. package/src/lib/model/ai-jobs.types.d.ts +3 -1
  105. package/src/lib/model/ai-jobs.types.js.map +1 -1
  106. package/src/lib/model/ai-logic-job.d.ts +18 -1
  107. package/src/lib/model/ai-logic-job.js +24 -3
  108. package/src/lib/model/ai-logic-job.js.map +1 -1
  109. package/src/lib/model/ai-media-credits.d.ts +8 -4
  110. package/src/lib/model/ai-media-credits.js +11 -4
  111. package/src/lib/model/ai-media-credits.js.map +1 -1
  112. package/src/lib/model/ai-media-kinds.d.ts +68 -0
  113. package/src/lib/model/ai-media-kinds.js +98 -0
  114. package/src/lib/model/ai-media-kinds.js.map +1 -0
  115. package/src/lib/model/ai-site-build-progress.d.ts +4 -0
  116. package/src/lib/model/ai-site-build-progress.js +28 -11
  117. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  118. package/src/lib/model/ai-site-context.d.ts +111 -0
  119. package/src/lib/model/ai-site-context.js +222 -0
  120. package/src/lib/model/ai-site-context.js.map +1 -0
  121. package/src/lib/model/ai-site-job.d.ts +10 -2
  122. package/src/lib/model/ai-site-job.js +12 -5
  123. package/src/lib/model/ai-site-job.js.map +1 -1
  124. package/src/lib/model/ai-site-kinds.d.ts +164 -0
  125. package/src/lib/model/ai-site-kinds.js +1942 -0
  126. package/src/lib/model/ai-site-kinds.js.map +1 -0
  127. package/src/lib/model/ai-site-look.d.ts +150 -0
  128. package/src/lib/model/ai-site-look.js +1236 -0
  129. package/src/lib/model/ai-site-look.js.map +1 -0
  130. package/src/lib/model/ai-site-memory.d.ts +80 -0
  131. package/src/lib/model/ai-site-memory.js +171 -0
  132. package/src/lib/model/ai-site-memory.js.map +1 -0
  133. package/src/lib/model/ai-site-start.d.ts +10 -1
  134. package/src/lib/model/ai-site-start.js +16 -2
  135. package/src/lib/model/ai-site-start.js.map +1 -1
  136. package/src/lib/model/assist-follow-up.d.ts +92 -0
  137. package/src/lib/model/assist-follow-up.js +128 -0
  138. package/src/lib/model/assist-follow-up.js.map +1 -0
  139. package/src/lib/plugin-config.js +2 -0
  140. package/src/lib/plugin-config.js.map +1 -1
  141. package/src/lib/plugin.js +20 -1
  142. package/src/lib/plugin.js.map +1 -1
  143. package/src/lib/providers/anthropic.d.ts +9 -1
  144. package/src/lib/providers/anthropic.js +143 -9
  145. package/src/lib/providers/anthropic.js.map +1 -1
  146. package/src/lib/providers/catalog.d.ts +37 -7
  147. package/src/lib/providers/catalog.js +103 -11
  148. package/src/lib/providers/catalog.js.map +1 -1
  149. package/src/lib/providers/contract.d.ts +29 -0
  150. package/src/lib/providers/contract.js.map +1 -1
  151. package/src/lib/providers/image-contract.d.ts +22 -1
  152. package/src/lib/providers/image-contract.js +40 -1
  153. package/src/lib/providers/image-contract.js.map +1 -1
  154. package/src/lib/providers/image-endpoints.d.ts +24 -0
  155. package/src/lib/providers/image-endpoints.js +28 -0
  156. package/src/lib/providers/image-endpoints.js.map +1 -0
  157. package/src/lib/providers/model-choice.js +23 -2
  158. package/src/lib/providers/model-choice.js.map +1 -1
  159. package/src/lib/providers/routing.js +18 -0
  160. package/src/lib/providers/routing.js.map +1 -1
  161. package/src/lib/providers/vertex-image-endpoint.d.ts +48 -0
  162. package/src/lib/providers/vertex-image-endpoint.js +80 -0
  163. package/src/lib/providers/vertex-image-endpoint.js.map +1 -0
  164. package/src/lib/providers/vertex-image.d.ts +13 -27
  165. package/src/lib/providers/vertex-image.js +15 -42
  166. package/src/lib/providers/vertex-image.js.map +1 -1
  167. package/src/lib/runtime/ai-blog-post-generation.d.ts +62 -0
  168. package/src/lib/runtime/ai-blog-post-generation.js +89 -0
  169. package/src/lib/runtime/ai-blog-post-generation.js.map +1 -0
  170. package/src/lib/runtime/ai-dev-env.d.ts +53 -0
  171. package/src/lib/runtime/ai-dev-env.js +72 -0
  172. package/src/lib/runtime/ai-dev-env.js.map +1 -0
  173. package/src/lib/runtime/ai-dev-replay.d.ts +116 -0
  174. package/src/lib/runtime/ai-dev-replay.js +313 -0
  175. package/src/lib/runtime/ai-dev-replay.js.map +1 -0
  176. package/src/lib/runtime/ai-doctrine.js +5 -1
  177. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  178. package/src/lib/runtime/ai-eval-live.js +4 -0
  179. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  180. package/src/lib/runtime/ai-eval.d.ts +13 -1
  181. package/src/lib/runtime/ai-eval.js +84 -1
  182. package/src/lib/runtime/ai-eval.js.map +1 -1
  183. package/src/lib/runtime/ai-live-batch.d.ts +45 -0
  184. package/src/lib/runtime/ai-live-batch.js +89 -0
  185. package/src/lib/runtime/ai-live-batch.js.map +1 -0
  186. package/src/lib/runtime/ai-runtime.js +46 -2
  187. package/src/lib/runtime/ai-runtime.js.map +1 -1
  188. package/src/lib/runtime/site-context.d.ts +58 -0
  189. package/src/lib/runtime/site-context.js +127 -0
  190. package/src/lib/runtime/site-context.js.map +1 -0
  191. package/src/lib/server/ai-admin-conversations.d.ts +22 -0
  192. package/src/lib/server/ai-admin-conversations.js +197 -0
  193. package/src/lib/server/ai-admin-conversations.js.map +1 -0
  194. package/src/lib/server/ai-media-image.d.ts +5 -5
  195. package/src/lib/server/ai-media-image.js +25 -11
  196. package/src/lib/server/ai-media-image.js.map +1 -1
  197. package/src/lib/server/ai-media-raster-prompt.d.ts +34 -0
  198. package/src/lib/server/ai-media-raster-prompt.js +53 -0
  199. package/src/lib/server/ai-media-raster-prompt.js.map +1 -0
  200. package/src/lib/server/assist-chat.d.ts +11 -0
  201. package/src/lib/server/assist-chat.js +57 -18
  202. package/src/lib/server/assist-chat.js.map +1 -1
  203. package/src/lib/server/assist-edit-applied.js +16 -3
  204. package/src/lib/server/assist-edit-applied.js.map +1 -1
  205. package/src/lib/server/assist-view-context.d.ts +19 -1
  206. package/src/lib/server/assist-view-context.js +38 -1
  207. package/src/lib/server/assist-view-context.js.map +1 -1
  208. package/src/lib/server.js +10 -0
  209. package/src/lib/server.js.map +1 -1
  210. package/src/lib/subprocessors.js +17 -1
  211. package/src/lib/subprocessors.js.map +1 -1
  212. package/src/lib/tools/ai-blog-post-tool.d.ts +60 -0
  213. package/src/lib/tools/ai-blog-post-tool.js +175 -0
  214. package/src/lib/tools/ai-blog-post-tool.js.map +1 -0
  215. package/src/lib/usage/assist-usage.d.ts +3 -0
  216. package/src/lib/usage/assist-usage.js +4 -2
  217. package/src/lib/usage/assist-usage.js.map +1 -1
  218. package/src/lib/usage/staff-org-ai-conversations.d.ts +103 -0
  219. package/src/lib/usage/staff-org-ai-conversations.js +152 -0
  220. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { AiTool } from '../providers/contract';
18
+ import type { AiGenerationCheckResult } from '../runtime/ai-doctrine';
19
+ /**
20
+ * The strict tool a site's first posts are written through (AGL-3676), and
21
+ * the check that holds each post to what the site's collection publishes.
22
+ *
23
+ * A post is published with the site, as its own words, so nothing in it may
24
+ * mark a gap for later: no square brackets, no link, no markup the entry
25
+ * renderer does not read. Its body is the markdown-lite the entry editor
26
+ * writes — `##` subheadings, `-` lists, `**bold**`, `*italic*` — and its
27
+ * title, excerpt and search description are plain text. The storefront copy
28
+ * rules hold every part (no health, financial or legal promise; no
29
+ * certification, award or endorsement the brief does not state; no price),
30
+ * and so does a rule of its own: no star rating, review score or testimonial
31
+ * nobody gave.
32
+ */
33
+ export declare const AI_BLOG_POST_TOOL_NAME = "write_blog_post";
34
+ /** A post's bounds, as the tool states them and its check holds them. */
35
+ export declare const AI_BLOG_POST_LIMITS: {
36
+ readonly titleMax: 80;
37
+ readonly excerptMax: 200;
38
+ readonly bodyMin: 600;
39
+ readonly bodyMax: 4000;
40
+ readonly seoDescriptionMax: number;
41
+ };
42
+ /** A post as its check reads it. */
43
+ export interface AiBlogPost {
44
+ title: string;
45
+ excerpt: string;
46
+ /** Markdown-lite. */
47
+ body: string;
48
+ seoDescription: string;
49
+ }
50
+ export declare const AI_BLOG_POST_TOOL: AiTool;
51
+ /**
52
+ * A post's answer held to the collection it is published in and the copy
53
+ * rules. `merchantWords` are the brief and the site's own answers, the only
54
+ * source a stated claim or price may come from; `earlierTitles` are the posts
55
+ * already written, which this one must not repeat.
56
+ */
57
+ export declare function checkAiBlogPost(answer: Record<string, unknown>, context: {
58
+ merchantWords: string;
59
+ earlierTitles?: readonly string[];
60
+ }): AiGenerationCheckResult<AiBlogPost>;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { SEO_LISTING_FIELDS } from "@aglyn/aglyn/app-utils/seo-listing-fields";
17
+ import { findInventedPrices, findStorefrontClaims, storefrontClaimViolations } from "../model/ai-storefront-claims.js";
18
+ import { detectOffVoiceCopy } from "../runtime/ai-doctrine-validators.js";
19
+ /**
20
+ * The strict tool a site's first posts are written through (AGL-3676), and
21
+ * the check that holds each post to what the site's collection publishes.
22
+ *
23
+ * A post is published with the site, as its own words, so nothing in it may
24
+ * mark a gap for later: no square brackets, no link, no markup the entry
25
+ * renderer does not read. Its body is the markdown-lite the entry editor
26
+ * writes — `##` subheadings, `-` lists, `**bold**`, `*italic*` — and its
27
+ * title, excerpt and search description are plain text. The storefront copy
28
+ * rules hold every part (no health, financial or legal promise; no
29
+ * certification, award or endorsement the brief does not state; no price),
30
+ * and so does a rule of its own: no star rating, review score or testimonial
31
+ * nobody gave.
32
+ */ export const AI_BLOG_POST_TOOL_NAME = 'write_blog_post';
33
+ /** A post's bounds, as the tool states them and its check holds them. */ export const AI_BLOG_POST_LIMITS = {
34
+ titleMax: 80,
35
+ excerptMax: 200,
36
+ bodyMin: 600,
37
+ bodyMax: 4000,
38
+ seoDescriptionMax: SEO_LISTING_FIELDS.description.maxLength
39
+ };
40
+ const string = (description)=>({
41
+ type: 'string',
42
+ description
43
+ });
44
+ export const AI_BLOG_POST_TOOL = {
45
+ name: AI_BLOG_POST_TOOL_NAME,
46
+ description: 'Write one post of a new blog. Every field is required.',
47
+ strict: true,
48
+ inputSchema: {
49
+ type: 'object',
50
+ additionalProperties: false,
51
+ required: [
52
+ 'title',
53
+ 'excerpt',
54
+ 'body',
55
+ 'seoDescription'
56
+ ],
57
+ properties: {
58
+ title: string(`Plain text, at most ${AI_BLOG_POST_LIMITS.titleMax} characters.`),
59
+ excerpt: string(`One or two plain sentences, at most ${AI_BLOG_POST_LIMITS.excerptMax} characters.`),
60
+ body: string(`Markdown-lite, ${AI_BLOG_POST_LIMITS.bodyMin} to ${AI_BLOG_POST_LIMITS.bodyMax} characters: short paragraphs, ## subheadings, - lists, **bold**. No title line.`),
61
+ seoDescription: string(`One plain sentence, at most ${AI_BLOG_POST_LIMITS.seoDescriptionMax} characters.`)
62
+ }
63
+ }
64
+ };
65
+ function fail(findings, at, code, message, value) {
66
+ findings.violations.push({
67
+ rule: null,
68
+ code,
69
+ message,
70
+ paths: [
71
+ at
72
+ ]
73
+ });
74
+ if (!(at in findings.offending)) findings.offending[at] = value;
75
+ }
76
+ const line = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
77
+ /** Paragraphs: spaces folded within a line, at most one blank line between paragraphs. */ const paragraphs = (value)=>typeof value === 'string' ? value.replace(/\r\n?/g, '\n').split('\n').map((part)=>part.replace(/[ \t\f\v]+/g, ' ').trimEnd()).join('\n').replace(/\n{3,}/g, '\n\n').trim() : '';
78
+ const HTML = /<\/?[a-z][^>]*>/i;
79
+ const PLAIN_MARKS = /(^|\n)\s*#{1,6}\s|\*\*[^*\n]+\*\*|__[^_\n]+__|`/;
80
+ const EMOJI = RegExp("[\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299\\u{1F004}\\u{1F02C}-\\u{1F02F}\\u{1F094}-\\u{1F09F}\\u{1F0AF}-\\u{1F0B0}\\u{1F0C0}\\u{1F0CF}-\\u{1F0D0}\\u{1F0F6}-\\u{1F0FF}\\u{1F170}-\\u{1F171}\\u{1F17E}-\\u{1F17F}\\u{1F18E}\\u{1F191}-\\u{1F19A}\\u{1F1AE}-\\u{1F1E5}\\u{1F201}-\\u{1F20F}\\u{1F21A}\\u{1F22F}\\u{1F232}-\\u{1F23A}\\u{1F23C}-\\u{1F23F}\\u{1F249}-\\u{1F25F}\\u{1F266}-\\u{1F321}\\u{1F324}-\\u{1F393}\\u{1F396}-\\u{1F397}\\u{1F399}-\\u{1F39B}\\u{1F39E}-\\u{1F3F0}\\u{1F3F3}-\\u{1F3F5}\\u{1F3F7}-\\u{1F3FA}\\u{1F400}-\\u{1F4FD}\\u{1F4FF}-\\u{1F53D}\\u{1F549}-\\u{1F54E}\\u{1F550}-\\u{1F567}\\u{1F56F}-\\u{1F570}\\u{1F573}-\\u{1F57A}\\u{1F587}\\u{1F58A}-\\u{1F58D}\\u{1F590}\\u{1F595}-\\u{1F596}\\u{1F5A4}-\\u{1F5A5}\\u{1F5A8}\\u{1F5B1}-\\u{1F5B2}\\u{1F5BC}\\u{1F5C2}-\\u{1F5C4}\\u{1F5D1}-\\u{1F5D3}\\u{1F5DC}-\\u{1F5DE}\\u{1F5E1}\\u{1F5E3}\\u{1F5E8}\\u{1F5EF}\\u{1F5F3}\\u{1F5FA}-\\u{1F64F}\\u{1F680}-\\u{1F6C5}\\u{1F6CB}-\\u{1F6D2}\\u{1F6D5}-\\u{1F6E5}\\u{1F6E9}\\u{1F6EB}-\\u{1F6F0}\\u{1F6F3}-\\u{1F6FF}\\u{1F7DA}-\\u{1F7FF}\\u{1F80C}-\\u{1F80F}\\u{1F848}-\\u{1F84F}\\u{1F85A}-\\u{1F85F}\\u{1F888}-\\u{1F88F}\\u{1F8AE}-\\u{1F8AF}\\u{1F8BC}-\\u{1F8BF}\\u{1F8C2}-\\u{1F8CF}\\u{1F8D9}-\\u{1F8FF}\\u{1F90C}-\\u{1F93A}\\u{1F93C}-\\u{1F945}\\u{1F947}-\\u{1F9FF}\\u{1FA58}-\\u{1FA5F}\\u{1FA6E}-\\u{1FAFF}\\u{1FC00}-\\u{1FFFD}]", "u");
81
+ const BRACKET = /[[\]]/;
82
+ const URL = /\bhttps?:\/\/|\bwww\./i;
83
+ /** A heading deeper than the entry renderer's subheadings, or a title line. */ const TITLE_HEADING = /(^|\n)\s*#\s/;
84
+ /** A rating or review score nobody gave. */ const RATING = /\b\d(?:\.\d)?\s*(?:\/\s*(?:5|10)\b|out of (?:5|five|10|ten)\b|stars?\b)|\b(?:five|5|four|4)[- ]star\b|\brated\b|\breviews? (?:say|rave|agree)\b/i;
85
+ /** One plain-text field: there, within its length, unmarked. */ function checkPlain(findings, at, raw, label, max) {
86
+ if (typeof raw !== 'string') {
87
+ fail(findings, at, 'type', `${label} must be text.`, raw);
88
+ return '';
89
+ }
90
+ const value = line(raw);
91
+ if (!value) {
92
+ fail(findings, at, 'missing', `${label} is required.`, raw);
93
+ return '';
94
+ }
95
+ if (value.length > max) fail(findings, at, 'too-long', `${label} is ${value.length} characters; it may have ${max}.`, raw);
96
+ if (HTML.test(value) || PLAIN_MARKS.test(value)) fail(findings, at, 'markup', `${label} is plain text, with no formatting marks.`, raw);
97
+ if (EMOJI.test(value)) fail(findings, at, 'emoji', `${label} holds no emoji.`, raw);
98
+ if (URL.test(value)) fail(findings, at, 'link', `${label} holds no link.`, raw);
99
+ if (BRACKET.test(value)) {
100
+ fail(findings, at, 'bracket', `${label} is published as written, so it marks no missing fact in brackets.`, raw);
101
+ }
102
+ return value;
103
+ }
104
+ /**
105
+ * A post's answer held to the collection it is published in and the copy
106
+ * rules. `merchantWords` are the brief and the site's own answers, the only
107
+ * source a stated claim or price may come from; `earlierTitles` are the posts
108
+ * already written, which this one must not repeat.
109
+ */ export function checkAiBlogPost(answer, context) {
110
+ var _context_earlierTitles;
111
+ const findings = {
112
+ violations: [],
113
+ offending: {}
114
+ };
115
+ const limits = AI_BLOG_POST_LIMITS;
116
+ const title = checkPlain(findings, 'title', answer['title'], 'The title', limits.titleMax);
117
+ if (title && ((_context_earlierTitles = context.earlierTitles) != null ? _context_earlierTitles : []).some((earlier)=>earlier.trim().toLowerCase() === title.toLowerCase())) {
118
+ fail(findings, 'title', 'title-duplicate', 'Another post already has this title; write about something else.', answer['title']);
119
+ }
120
+ const excerpt = checkPlain(findings, 'excerpt', answer['excerpt'], 'The excerpt', limits.excerptMax);
121
+ const seoDescription = checkPlain(findings, 'seoDescription', answer['seoDescription'], 'The search description', limits.seoDescriptionMax);
122
+ const raw = answer['body'];
123
+ const body = paragraphs(raw);
124
+ if (typeof raw !== 'string' || !body) {
125
+ fail(findings, 'body', 'missing', 'The body is required.', raw);
126
+ } else {
127
+ if (body.length < limits.bodyMin || body.length > limits.bodyMax) {
128
+ fail(findings, 'body', 'length', `The body is ${body.length} characters; write ${limits.bodyMin} to ${limits.bodyMax}.`, raw);
129
+ }
130
+ if (HTML.test(body)) fail(findings, 'body', 'markup', 'The body is markdown-lite, with no HTML.', raw);
131
+ if (TITLE_HEADING.test(body)) fail(findings, 'body', 'title-line', 'The title renders on its own; the body uses ## subheadings only.', raw);
132
+ if (EMOJI.test(body)) fail(findings, 'body', 'emoji', 'The body holds no emoji.', raw);
133
+ if (URL.test(body)) fail(findings, 'body', 'link', 'The body holds no link.', raw);
134
+ if (BRACKET.test(body)) {
135
+ fail(findings, 'body', 'bracket', 'The body is published as written, so it holds no square brackets: no links and no missing fact marked.', raw);
136
+ }
137
+ }
138
+ const samples = [
139
+ {
140
+ at: 'title',
141
+ text: title
142
+ },
143
+ {
144
+ at: 'excerpt',
145
+ text: excerpt
146
+ },
147
+ {
148
+ at: 'body',
149
+ text: body
150
+ },
151
+ {
152
+ at: 'seoDescription',
153
+ text: seoDescription
154
+ }
155
+ ];
156
+ for (const sample of samples){
157
+ const rating = RATING.exec(sample.text);
158
+ if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {
159
+ fail(findings, sample.at, 'rating-invented', `The post gives a rating or review nobody gave. Remove: "${rating[0]}".`, sample.text);
160
+ }
161
+ }
162
+ findings.violations.push(...storefrontClaimViolations(findStorefrontClaims(samples, context.merchantWords), findInventedPrices(samples, context.merchantWords)), ...detectOffVoiceCopy(samples, null));
163
+ return {
164
+ value: findings.violations.length ? null : {
165
+ title,
166
+ excerpt,
167
+ body,
168
+ seoDescription
169
+ },
170
+ violations: findings.violations,
171
+ offending: findings.offending
172
+ };
173
+ }
174
+
175
+ //# sourceMappingURL=ai-blog-post-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-blog-post-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 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 tool a site's first posts are written through (AGL-3676), and\n * the check that holds each post to what the site's collection publishes.\n *\n * A post is published with the site, as its own words, so nothing in it may\n * mark a gap for later: no square brackets, no link, no markup the entry\n * renderer does not read. Its body is the markdown-lite the entry editor\n * writes — `##` subheadings, `-` lists, `**bold**`, `*italic*` — and its\n * title, excerpt and search description are plain text. The storefront copy\n * rules hold every part (no health, financial or legal promise; no\n * certification, award or endorsement the brief does not state; no price),\n * and so does a rule of its own: no star rating, review score or testimonial\n * nobody gave.\n */\n\nexport const AI_BLOG_POST_TOOL_NAME = 'write_blog_post'\n\n/** A post's bounds, as the tool states them and its check holds them. */\nexport const AI_BLOG_POST_LIMITS = {\n titleMax: 80,\n excerptMax: 200,\n bodyMin: 600,\n bodyMax: 4_000,\n seoDescriptionMax: SEO_LISTING_FIELDS.description.maxLength,\n} as const\n\n/** A post as its check reads it. */\nexport interface AiBlogPost {\n title: string\n excerpt: string\n /** Markdown-lite. */\n body: string\n seoDescription: string\n}\n\nconst string = (description: string) => ({ type: 'string', description })\n\nexport const AI_BLOG_POST_TOOL: AiTool = {\n name: AI_BLOG_POST_TOOL_NAME,\n description: 'Write one post of a new blog. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['title', 'excerpt', 'body', 'seoDescription'],\n properties: {\n title: string(`Plain text, at most ${AI_BLOG_POST_LIMITS.titleMax} characters.`),\n excerpt: string(`One or two plain sentences, at most ${AI_BLOG_POST_LIMITS.excerptMax} characters.`),\n body: string(\n `Markdown-lite, ${AI_BLOG_POST_LIMITS.bodyMin} to ${AI_BLOG_POST_LIMITS.bodyMax} characters: short paragraphs, ## subheadings, - lists, **bold**. No title line.`,\n ),\n seoDescription: string(`One plain sentence, at most ${AI_BLOG_POST_LIMITS.seoDescriptionMax} characters.`),\n },\n },\n}\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\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, ' ').trimEnd())\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n : ''\n\nconst HTML = /<\\/?[a-z][^>]*>/i\nconst PLAIN_MARKS = /(^|\\n)\\s*#{1,6}\\s|\\*\\*[^*\\n]+\\*\\*|__[^_\\n]+__|`/\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n/** A heading deeper than the entry renderer's subheadings, or a title line. */\nconst TITLE_HEADING = /(^|\\n)\\s*#\\s/\n/** A rating or review score nobody gave. */\nconst RATING = /\\b\\d(?:\\.\\d)?\\s*(?:\\/\\s*(?:5|10)\\b|out of (?:5|five|10|ten)\\b|stars?\\b)|\\b(?:five|5|four|4)[- ]star\\b|\\brated\\b|\\breviews? (?:say|rave|agree)\\b/i\n\n/** One plain-text field: there, within its length, unmarked. */\nfunction checkPlain(findings: Findings, at: string, raw: unknown, label: string, max: number): string {\n if (typeof raw !== 'string') {\n fail(findings, at, 'type', `${label} must be text.`, raw)\n return ''\n }\n const value = line(raw)\n if (!value) {\n fail(findings, at, 'missing', `${label} is required.`, raw)\n return ''\n }\n if (value.length > max) fail(findings, at, 'too-long', `${label} is ${value.length} characters; it may have ${max}.`, raw)\n if (HTML.test(value) || PLAIN_MARKS.test(value)) fail(findings, at, 'markup', `${label} is plain text, with no formatting marks.`, raw)\n if (EMOJI.test(value)) fail(findings, at, 'emoji', `${label} holds no emoji.`, raw)\n if (URL.test(value)) fail(findings, at, 'link', `${label} holds no link.`, raw)\n if (BRACKET.test(value)) {\n fail(findings, at, 'bracket', `${label} is published as written, so it marks no missing fact in brackets.`, raw)\n }\n return value\n}\n\n/**\n * A post's answer held to the collection it is published in and the copy\n * rules. `merchantWords` are the brief and the site's own answers, the only\n * source a stated claim or price may come from; `earlierTitles` are the posts\n * already written, which this one must not repeat.\n */\nexport function checkAiBlogPost(\n answer: Record<string, unknown>,\n context: { merchantWords: string; earlierTitles?: readonly string[] },\n): AiGenerationCheckResult<AiBlogPost> {\n const findings: Findings = { violations: [], offending: {} }\n const limits = AI_BLOG_POST_LIMITS\n const title = checkPlain(findings, 'title', answer['title'], 'The title', limits.titleMax)\n if (title && (context.earlierTitles ?? []).some((earlier) => earlier.trim().toLowerCase() === title.toLowerCase())) {\n fail(findings, 'title', 'title-duplicate', 'Another post already has this title; write about something else.', answer['title'])\n }\n const excerpt = checkPlain(findings, 'excerpt', answer['excerpt'], 'The excerpt', limits.excerptMax)\n const seoDescription = checkPlain(\n findings,\n 'seoDescription',\n answer['seoDescription'],\n 'The search description',\n limits.seoDescriptionMax,\n )\n const raw = answer['body']\n const body = paragraphs(raw)\n if (typeof raw !== 'string' || !body) {\n fail(findings, 'body', 'missing', 'The body is required.', raw)\n } else {\n if (body.length < limits.bodyMin || body.length > limits.bodyMax) {\n fail(findings, 'body', 'length', `The body is ${body.length} characters; write ${limits.bodyMin} to ${limits.bodyMax}.`, raw)\n }\n if (HTML.test(body)) fail(findings, 'body', 'markup', 'The body is markdown-lite, with no HTML.', raw)\n if (TITLE_HEADING.test(body)) fail(findings, 'body', 'title-line', 'The title renders on its own; the body uses ## subheadings only.', raw)\n if (EMOJI.test(body)) fail(findings, 'body', 'emoji', 'The body holds no emoji.', raw)\n if (URL.test(body)) fail(findings, 'body', 'link', 'The body holds no link.', raw)\n if (BRACKET.test(body)) {\n fail(findings, 'body', 'bracket', 'The body is published as written, so it holds no square brackets: no links and no missing fact marked.', raw)\n }\n }\n const samples: AiStorefrontCopySample[] = [\n { at: 'title', text: title },\n { at: 'excerpt', text: excerpt },\n { at: 'body', text: body },\n { at: 'seoDescription', text: seoDescription },\n ]\n for (const sample of samples) {\n const rating = RATING.exec(sample.text)\n if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {\n fail(findings, sample.at, 'rating-invented', `The post gives a rating or review nobody gave. Remove: \"${rating[0]}\".`, sample.text)\n }\n }\n findings.violations.push(\n ...storefrontClaimViolations(\n findStorefrontClaims(samples, context.merchantWords),\n findInventedPrices(samples, context.merchantWords),\n ),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { title, excerpt, body, seoDescription },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["SEO_LISTING_FIELDS","findInventedPrices","findStorefrontClaims","storefrontClaimViolations","detectOffVoiceCopy","AI_BLOG_POST_TOOL_NAME","AI_BLOG_POST_LIMITS","titleMax","excerptMax","bodyMin","bodyMax","seoDescriptionMax","description","maxLength","string","type","AI_BLOG_POST_TOOL","name","strict","inputSchema","additionalProperties","required","properties","title","excerpt","body","seoDescription","fail","findings","at","code","message","value","violations","push","rule","paths","offending","line","replace","trim","paragraphs","split","map","part","trimEnd","join","HTML","PLAIN_MARKS","EMOJI","BRACKET","URL","TITLE_HEADING","RATING","checkPlain","raw","label","max","length","test","checkAiBlogPost","answer","context","limits","earlierTitles","some","earlier","toLowerCase","samples","text","sample","rating","exec","merchantWords","includes"],"mappings":"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"}
@@ -18,6 +18,7 @@ import { type PublicAssistCredits } from './assist-credits';
18
18
  import { type AiOverageCapReason } from '../billing/ai-overage-gate';
19
19
  import type { AiRefusedBy } from '../model/ai-allotments';
20
20
  import { type AiAllotmentGate, type AiAllotmentRequestSubject } from './ai-allotments';
21
+ import type { AiImageSize } from '../providers/image-contract';
21
22
  import { type AssistMeteredOrg, type FreeAssistAccount } from './assist-free-taste';
22
23
  import { type AiUsageKind } from '../model/ai-usage-by-user';
23
24
  export { assistMonthlyCeilingUsd } from './assist-ceiling';
@@ -153,6 +154,8 @@ export interface AssistTokenUsage {
153
154
  * catalog. Absent on every text exchange.
154
155
  */
155
156
  images?: number;
157
+ /** The size those pictures were made at (AGL-3602); 1K when absent. */
158
+ imageSize?: AiImageSize;
156
159
  }
157
160
  /**
158
161
  * What one exchange DRAWS FROM THE CUSTOMER, at the serving model's billed
@@ -793,9 +793,11 @@ async function reserveInTransaction(firestore, orgId, entitled, now, org, subjec
793
793
  feedback: null
794
794
  }, record.editOps && record.editOps > 0 ? {
795
795
  editOps: Math.floor(record.editOps)
796
- } : {}, record.usage.images && record.usage.images > 0 ? {
796
+ } : {}, record.usage.images && record.usage.images > 0 ? _extends({
797
797
  images: Math.floor(record.usage.images)
798
- } : {}, {
798
+ }, record.usage.imageSize ? {
799
+ imageSize: record.usage.imageSize
800
+ } : {}) : {}, {
799
801
  createdAt: serverTimestamp()
800
802
  }));
801
803
  // NO message counting here (AGL-2057). Both message counters are moved by