@aglyn/plugins-ai 1.0.0-beta.234 → 1.0.0-beta.236

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 (137) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +25 -0
  3. package/src/lib/activity/ai-activity-actions.js +85 -4
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-brief-dialog.component.js +46 -2
  6. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  7. package/src/lib/components/ai-campaign-create.component.js +1 -1
  8. package/src/lib/components/ai-campaign-create.component.js.map +1 -1
  9. package/src/lib/components/ai-credits-card.component.js +3 -3
  10. package/src/lib/components/ai-credits-card.component.js.map +1 -1
  11. package/src/lib/components/ai-crm-email-draft.component.js +1 -1
  12. package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
  13. package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
  14. package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
  15. package/src/lib/components/ai-crm-record-card.component.js +1 -1
  16. package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
  17. package/src/lib/components/ai-describe-automation.component.js +4 -2
  18. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  19. package/src/lib/components/ai-describe-org-automation.component.js +1 -1
  20. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
  21. package/src/lib/components/ai-experiment-cards.component.js +2 -2
  22. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  23. package/src/lib/components/ai-explain-automation.component.js +1 -1
  24. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  25. package/src/lib/components/ai-insight-card.component.js +3 -1
  26. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  27. package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
  28. package/src/lib/components/ai-jobs-list-page.component.js +15 -2
  29. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  30. package/src/lib/components/ai-logic.component.js +5 -3
  31. package/src/lib/components/ai-logic.component.js.map +1 -1
  32. package/src/lib/components/ai-media-create.component.js +88 -16
  33. package/src/lib/components/ai-media-create.component.js.map +1 -1
  34. package/src/lib/components/ai-model-selector.component.js +8 -0
  35. package/src/lib/components/ai-model-selector.component.js.map +1 -1
  36. package/src/lib/components/ai-overlay-cards.component.js +2 -2
  37. package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
  38. package/src/lib/components/ai-product-copy-card.component.js +2 -1
  39. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  40. package/src/lib/components/ai-products-hub-card.component.js +2 -1
  41. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  42. package/src/lib/components/ai-revise-automation.component.js +1 -1
  43. package/src/lib/components/ai-revise-automation.component.js.map +1 -1
  44. package/src/lib/components/ai-seo-fields-card.component.js +7 -3
  45. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  46. package/src/lib/components/ai-site-batch-card.component.js +7 -4
  47. package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
  48. package/src/lib/components/ai-site-build-page.component.js +7 -3
  49. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  50. package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
  51. package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
  52. package/src/lib/components/ai-site-start-card.component.js +62 -29
  53. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  54. package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
  55. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  56. package/src/lib/components/ai-upsell-dialog.component.js +17 -3
  57. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  58. package/src/lib/components/ai-usage-strip.component.js +13 -2
  59. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  60. package/src/lib/components/assist-build-card.component.js +20 -3
  61. package/src/lib/components/assist-build-card.component.js.map +1 -1
  62. package/src/lib/components/assist-jobs-drawer.component.js +7 -1
  63. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  64. package/src/lib/components/billing-ai-allotments.component.js +2 -2
  65. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  66. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  67. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  68. package/src/lib/components/billing-assist-overage-card.component.js +2 -2
  69. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  70. package/src/lib/components/host-ai-allotments.component.js +2 -2
  71. package/src/lib/components/host-ai-allotments.component.js.map +1 -1
  72. package/src/lib/components/member-ai-allotment-card.component.js +2 -2
  73. package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
  74. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  75. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  76. package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
  77. package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
  78. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  79. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  80. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  81. package/src/lib/jobs/ai-job-layout-language.js +4 -1
  82. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  83. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +12 -0
  84. package/src/lib/jobs/ai-job-layout-site-pages.js +20 -0
  85. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  86. package/src/lib/jobs/ai-job-page-language.d.ts +10 -1
  87. package/src/lib/jobs/ai-job-page-language.js +41 -8
  88. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  89. package/src/lib/jobs/ai-job-page-step.js +14 -3
  90. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  91. package/src/lib/jobs/ai-job-site-content.d.ts +75 -7
  92. package/src/lib/jobs/ai-job-site-content.js +205 -32
  93. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  94. package/src/lib/jobs/ai-job-site-step.d.ts +8 -0
  95. package/src/lib/jobs/ai-job-site-step.js +61 -12
  96. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-compiler.d.ts +8 -0
  98. package/src/lib/layout-language/ai-layout-compiler.js +308 -10
  99. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  100. package/src/lib/layout-language/ai-layout-design.d.ts +7 -0
  101. package/src/lib/layout-language/ai-layout-design.js +23 -11
  102. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  103. package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
  104. package/src/lib/layout-language/ai-layout-frame.js +12 -3
  105. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  106. package/src/lib/layout-language/ai-layout-gaps.js +18 -2
  107. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  108. package/src/lib/layout-language/ai-layout-links.d.ts +26 -1
  109. package/src/lib/layout-language/ai-layout-links.js +24 -4
  110. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  111. package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
  112. package/src/lib/layout-language/ai-layout-listings.js +187 -0
  113. package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
  114. package/src/lib/model/ai-site-build-progress.js +27 -12
  115. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  116. package/src/lib/model/ai-site-job.d.ts +33 -8
  117. package/src/lib/model/ai-site-job.js +38 -18
  118. package/src/lib/model/ai-site-job.js.map +1 -1
  119. package/src/lib/model/ai-site-kinds.d.ts +30 -8
  120. package/src/lib/model/ai-site-kinds.js +731 -117
  121. package/src/lib/model/ai-site-kinds.js.map +1 -1
  122. package/src/lib/model/ai-site-start-seo.d.ts +30 -0
  123. package/src/lib/model/ai-site-start-seo.js +132 -11
  124. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  125. package/src/lib/model/ai-site-start.d.ts +2 -2
  126. package/src/lib/model/ai-site-start.js +3 -3
  127. package/src/lib/model/ai-site-start.js.map +1 -1
  128. package/src/lib/plugin.js +15 -3
  129. package/src/lib/plugin.js.map +1 -1
  130. package/src/lib/runtime/ai-palette.generated.js +13 -3
  131. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  132. package/src/lib/server/ai-admin-conversations.d.ts +9 -0
  133. package/src/lib/server/ai-admin-conversations.js +151 -9
  134. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  135. package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
  136. package/src/lib/usage/staff-org-ai-conversations.js +45 -0
  137. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
@@ -19,11 +19,13 @@ import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-re
19
19
  import { publishContentEntries, writeContentCollection, writeContentEntryDraft } from "@aglyn/tenant-data-admin/server/content-entry-drafts";
20
20
  import { aiProductsProposalOf } from "../model/ai-products.js";
21
21
  import { aiSiteKindOfInputs } from "../model/ai-site-kinds.js";
22
- import { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
22
+ import { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS, aiSiteWords } from "../model/ai-site-job.js";
23
23
  import { AI_STEP_TIERS } from "../providers/catalog.js";
24
24
  import { aiModelForStep } from "../providers/routing.js";
25
25
  import { AI_BLOG_POST_MAX_TOKENS, AI_BLOG_POST_STEP, generateAiBlogPost } from "../runtime/ai-blog-post-generation.js";
26
26
  import { aiLayoutStarterPhotos } from "../layout-language/ai-layout-pictures.js";
27
+ import { aiLayoutStockPhotoSource } from "./ai-layout-stock-photos.js";
28
+ import { aiLayoutListingId, aiLayoutListingPlacements } from "../layout-language/ai-layout-listings.js";
27
29
  import { aiJobAdmissionFor } from "./ai-job-admission.js";
28
30
  import { aiJobStepBudget } from "./ai-job-budget.js";
29
31
  import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
@@ -41,18 +43,25 @@ import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
41
43
  * `AI_SITE_POSTS` posts in it, one generation a pass. Each is written as a
42
44
  * draft through the same rules the console's routes hold
43
45
  * (`content-entry-drafts.ts`), bylined with the business's name the person
44
- * gave, and given a starter photo as its cover. They go live with the
46
+ * gave, and given a cover: a stock photo of its own title where the
47
+ * deployment has a library, else a starter (AGL-3676), which the blog's
48
+ * cards show. They go live with the
45
49
  * site: the guided start's publish publishes them with its pages
46
50
  * (`aiPublishSitePosts`), as a person's Publish would.
47
51
  * - ADDING YOUR FIRST PRODUCTS: the `products` step's catalog, asked for
48
52
  * `AI_SITE_PRODUCTS` products, each written through the commerce plugin's
49
- * `product` draft writer — a DRAFT with no price, off the storefront until
50
- * the owner prices it, since no price is ever invented. No photo either:
51
- * the writer takes none, and a stock picture of something else would show
52
- * a shopper a product that is not the one for sale.
53
+ * `product` draft writer. The catalog proposes no price, so each takes
54
+ * the writer's default price (Zach, 2026-10-08: "default a price",
55
+ * AGL-3676) for the owner to change; the job's row says so. Since the
56
+ * 2026-10-08 Ember & Wick start, whose store showed no product at all,
57
+ * each is LISTED at once (`comingSoon`: written `active`). Each gets a
58
+ * photo in its slot, a stock photo of its own words where the deployment
59
+ * has a library, else a starter, for the owner to replace with their
60
+ * own.
53
61
  *
54
- * The pages built after them are told their titles or names, so the writing
55
- * page and the shop feature what exists rather than inventing their own.
62
+ * The pages built after them are told their titles or names, and the
63
+ * sections that show them list the real records (`aiSiteListings`,
64
+ * `ai-layout-listings.ts`): the shop's Product grid, the blog's posts.
56
65
  *
57
66
  * ── Not on the Free taste ────────────────────────────────────────────────
58
67
  *
@@ -124,19 +133,75 @@ const limitReview = (message)=>({
124
133
  message,
125
134
  findings: []
126
135
  });
136
+ /** A post's cover's shape: the blog's cards' (`COLLECTION_LIST_COVER_RATIO`, the posts listing's). */ const AI_SITE_POST_COVER_ASPECT = 3 / 2;
137
+ /**
138
+ * A post's cover (AGL-3676): a stock photo of the post's own title, copied
139
+ * into the site's library, where the deployment has a stock library; else the
140
+ * starter photo a post was always given, turned by the job so blogs differ.
141
+ * The blog's cards and the post's page show it, for the owner to replace.
142
+ * Never throws: a post whose search fails takes its starter.
143
+ */ export async function aiSitePostCover(input) {
144
+ var _ref;
145
+ var _starters_index;
146
+ const { job, index } = input;
147
+ if (job.hostId && input.title.trim()) {
148
+ try {
149
+ var _input_stockPhotos;
150
+ const source = ((_input_stockPhotos = input.stockPhotos) != null ? _input_stockPhotos : aiLayoutStockPhotoSource)(_extends({
151
+ hostId: job.hostId,
152
+ uid: job.createdBy,
153
+ seed: `${job.$id}:posts:${index}`,
154
+ business: aiSiteWords(job.inputs).about || job.brief,
155
+ sectionNames: [
156
+ 'Blog'
157
+ ]
158
+ }, input.signal ? {
159
+ signal: input.signal
160
+ } : {}));
161
+ const [found] = source ? await source([
162
+ {
163
+ imageId: `cover${index}`,
164
+ frameId: null,
165
+ iconId: null,
166
+ alt: input.title,
167
+ aspect: AI_SITE_POST_COVER_ASPECT,
168
+ sectionIndex: 0,
169
+ role: 'gallery'
170
+ }
171
+ ]) : [];
172
+ if (found == null ? void 0 : found.src) return found.src;
173
+ } catch (error) {
174
+ console.warn('ai site posts: the stock photo failed; the starter covers it', {
175
+ error: String(error)
176
+ });
177
+ }
178
+ }
179
+ const starters = aiLayoutStarterPhotos(Array.from({
180
+ length: Math.max(input.total, index + 1)
181
+ }, (_, slot)=>({
182
+ imageId: `cover${slot}`,
183
+ frameId: null,
184
+ iconId: null,
185
+ alt: '',
186
+ aspect: 16 / 9,
187
+ sectionIndex: slot + 1,
188
+ role: 'gallery'
189
+ })), job.$id);
190
+ return (_ref = (_starters_index = starters[index]) == null ? void 0 : _starters_index.src) != null ? _ref : null;
191
+ }
127
192
  /**
128
193
  * The posts unit's runner, on the job the scaffold derived for it: one post a
129
194
  * pass, continuing while posts remain. Its first pass makes the blog; every
130
195
  * pass finds it again under the unit's own id, and a post under its own id,
131
196
  * so a pass repeated after its write reports the post it already wrote.
132
197
  */ export function createAiSitePostsRunner(deps = {}) {
133
- var _deps_generate, _deps_writeCollection, _deps_writeEntry;
198
+ var _deps_generate, _deps_writeCollection, _deps_writeEntry, _deps_cover;
134
199
  const generate = (_deps_generate = deps.generate) != null ? _deps_generate : generateAiBlogPost;
135
200
  const writeCollection = (_deps_writeCollection = deps.writeCollection) != null ? _deps_writeCollection : writeContentCollection;
136
201
  const writeEntry = (_deps_writeEntry = deps.writeEntry) != null ? _deps_writeEntry : writeContentEntryDraft;
202
+ const coverFor = (_deps_cover = deps.cover) != null ? _deps_cover : aiSitePostCover;
137
203
  return async (context)=>{
138
204
  var _ref;
139
- var _covers_index;
140
205
  const { job, firestore, signal } = context;
141
206
  const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, AI_BLOG_POST_STEP)) != null ? _ref : aiModelForStep(AI_BLOG_POST_STEP);
142
207
  if (!job.hostId) return _extends({}, aiUnspentOutcome(model), {
@@ -180,18 +245,15 @@ const limitReview = (message)=>({
180
245
  failure: generation.message
181
246
  });
182
247
  const post = generation.value;
183
- // A starter photo as its cover, turned by the job so blogs differ.
184
- const covers = aiLayoutStarterPhotos(Array.from({
185
- length: input.total
186
- }, (_, slot)=>({
187
- imageId: `cover${slot}`,
188
- frameId: null,
189
- iconId: null,
190
- alt: '',
191
- aspect: 16 / 9,
192
- sectionIndex: slot + 1,
193
- role: 'gallery'
194
- })), job.$id);
248
+ // A stock photo of its title as its cover, else a starter (AGL-3676).
249
+ const cover = await coverFor(_extends({
250
+ job,
251
+ title: post.title,
252
+ index,
253
+ total: input.total
254
+ }, signal ? {
255
+ signal
256
+ } : {})).catch(()=>null);
195
257
  const byline = input.byline || site.name;
196
258
  const written = await writeEntry(firestore, {
197
259
  hostId: job.hostId,
@@ -202,9 +264,11 @@ const limitReview = (message)=>({
202
264
  title: post.title,
203
265
  excerpt: post.excerpt,
204
266
  body: post.body,
205
- seoDescription: post.seoDescription,
206
- coverImage: (_covers_index = covers[index]) == null ? void 0 : _covers_index.src
207
- }, byline ? {
267
+ seoDescription: post.seoDescription
268
+ }, cover ? {
269
+ coverImage: cover,
270
+ coverImageAlt: post.title
271
+ } : {}, byline ? {
208
272
  authorName: byline
209
273
  } : {}),
210
274
  now: context.now
@@ -235,6 +299,53 @@ const limitReview = (message)=>({
235
299
  };
236
300
  }
237
301
  export const runAiSitePostsUnit = createAiSitePostsRunner();
302
+ /**
303
+ * A photo for each product (AGL-3676), in order: a stock photo of the
304
+ * product's own words, copied into the site's library, where the deployment
305
+ * has a stock library; else a starter photo, as a page's empty picture takes
306
+ * one. It fills the photo slot every product card and product page has, for
307
+ * the owner to replace with their own; the product's note says so. Never
308
+ * throws: a product with no photo is still written.
309
+ */ export async function aiSiteProductPhotos(input) {
310
+ if (!input.names.length || !input.job.hostId) return input.names.map(()=>null);
311
+ const slots = input.names.map((name, index)=>({
312
+ imageId: `product${index}`,
313
+ frameId: null,
314
+ iconId: null,
315
+ alt: name,
316
+ // The product card's own shape (`cardStyle: 'photo'`).
317
+ aspect: 4 / 5,
318
+ sectionIndex: 0,
319
+ role: 'gallery'
320
+ }));
321
+ const seed = `${input.job.$id}:products`;
322
+ let found = [];
323
+ try {
324
+ var _input_stockPhotos;
325
+ const source = ((_input_stockPhotos = input.stockPhotos) != null ? _input_stockPhotos : aiLayoutStockPhotoSource)(_extends({
326
+ hostId: input.job.hostId,
327
+ uid: input.job.createdBy,
328
+ seed,
329
+ business: aiSiteWords(input.job.inputs).about || input.job.brief,
330
+ sectionNames: [
331
+ 'Products'
332
+ ]
333
+ }, input.signal ? {
334
+ signal: input.signal
335
+ } : {}));
336
+ if (source) found = await source(slots);
337
+ } catch (error) {
338
+ console.warn('ai site products: the stock photos failed; starter photos fill them', {
339
+ error: String(error)
340
+ });
341
+ }
342
+ const starters = aiLayoutStarterPhotos(slots, seed);
343
+ return slots.map((_slot, index)=>{
344
+ var _ref, _ref1;
345
+ var _found_index, _starters_index;
346
+ return (_ref = (_ref1 = (_found_index = found[index]) == null ? void 0 : _found_index.src) != null ? _ref1 : (_starters_index = starters[index]) == null ? void 0 : _starters_index.src) != null ? _ref : null;
347
+ });
348
+ }
238
349
  /** The sentence the products unit's brief ends with, which the catalog's rules read as how many. */ export function aiSiteProductsBriefLine(input = AI_SITE_PRODUCTS) {
239
350
  return `Propose between ${input.min} and ${input.max} products: the store's first ones.`;
240
351
  }
@@ -248,7 +359,7 @@ export const runAiSitePostsUnit = createAiSitePostsRunner();
248
359
  var _deps_writerFor;
249
360
  const writerFor = (_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter;
250
361
  return async (context)=>{
251
- var _ref;
362
+ var _ref, _deps_photos;
252
363
  var _catalog_proposal;
253
364
  const { job } = context;
254
365
  const keeper = writerFor('product');
@@ -276,10 +387,17 @@ export const runAiSitePostsUnit = createAiSitePostsRunner();
276
387
  outputs: []
277
388
  });
278
389
  const proposed = catalog.proposal.products.slice(0, AI_SITE_PRODUCTS.max);
390
+ const photos = await ((_deps_photos = deps.photos) != null ? _deps_photos : aiSiteProductPhotos)(_extends({
391
+ job,
392
+ names: proposed.map((product)=>product.name)
393
+ }, context.signal ? {
394
+ signal: context.signal
395
+ } : {})).catch(()=>proposed.map(()=>null));
279
396
  const outputs = [];
280
397
  let stopped = null;
281
398
  for (const [index, product] of proposed.entries()){
282
399
  var _context_org, _catalog_output_hostSubdomain;
400
+ const photo = photos[index];
283
401
  const written = await keeper.writer.write({
284
402
  orgId: job.orgId,
285
403
  hostId: job.hostId,
@@ -288,16 +406,25 @@ export const runAiSitePostsUnit = createAiSitePostsRunner();
288
406
  now: context.now,
289
407
  id: aiSiteProductId(job.$id, index),
290
408
  name: product.name,
291
- // No price: the owner sets it. No photo: the writer takes none.
292
- content: {
409
+ // No price stated: the writer gives it the store's default price
410
+ // for the owner to change (AGL-3676). Listed at once. Its photo
411
+ // slot holds a stock or starter photo for the owner to replace.
412
+ content: _extends({
293
413
  name: product.name,
294
414
  type: product.type,
295
- description: product.description,
415
+ // Listed at once, so no "[gift card terms]" gap reaches a shopper:
416
+ // a sentence that leaves a fact for the owner is left out.
417
+ description: aiSiteProductDescriptionWithoutGaps(product.description),
296
418
  tags: product.tags,
297
419
  options: product.options,
298
420
  seoTitle: product.seoTitle,
299
- seoDescription: product.seoDescription
300
- }
421
+ seoDescription: product.seoDescription,
422
+ comingSoon: true
423
+ }, photo ? {
424
+ mediaUrls: [
425
+ photo
426
+ ]
427
+ } : {})
301
428
  });
302
429
  if (written.ok === false) {
303
430
  stopped = written.error;
@@ -309,7 +436,7 @@ export const runAiSitePostsUnit = createAiSitePostsRunner();
309
436
  hostId: job.hostId,
310
437
  hostSubdomain: (_catalog_output_hostSubdomain = catalog.output.hostSubdomain) != null ? _catalog_output_hostSubdomain : null,
311
438
  label: written.name,
312
- note: 'A draft with no price. Set its price and photo in Products to put it on sale.'
439
+ note: AI_SITE_PRODUCT_NOTE
313
440
  });
314
441
  }
315
442
  if (!outputs.length) {
@@ -419,6 +546,52 @@ export const runAiSitePostsUnit = createAiSitePostsRunner();
419
546
  return null;
420
547
  }
421
548
  }
549
+ /**
550
+ * The site's listings a layout or a page unit is handed (AGL-3676,
551
+ * `ai-layout-listings.ts`): the store's catalog once its products are written,
552
+ * the blog once its posts are, each with the sections of the plan's pages
553
+ * that list it. A layout is built before either, so it is told the store will
554
+ * sell (`sells`, its ledger still owing the products part), which puts the
555
+ * cart in its header.
556
+ */ export function aiSiteListings(input) {
557
+ var _posts__proposal, _posts_;
558
+ const listings = [];
559
+ const products = input.outputs.filter((output)=>output.resource === 'product' && !output.proposal);
560
+ if (products.length || input.sells) {
561
+ listings.push({
562
+ id: aiLayoutListingId('products'),
563
+ kind: 'products',
564
+ name: 'the shop',
565
+ records: products.map((product)=>product.label),
566
+ placements: products.length ? aiLayoutListingPlacements('products', input.screens) : []
567
+ });
568
+ }
569
+ const posts = input.outputs.filter((output)=>output.resource === 'entry');
570
+ const slug = str((_posts_ = posts[0]) == null ? void 0 : (_posts__proposal = _posts_.proposal) == null ? void 0 : _posts__proposal['collectionSlug']);
571
+ if (posts.length && slug) {
572
+ listings.push({
573
+ id: aiLayoutListingId('posts'),
574
+ kind: 'posts',
575
+ name: 'the blog',
576
+ records: posts.map((post)=>post.label),
577
+ href: `/${slug}`,
578
+ collectionSlug: slug,
579
+ placements: aiLayoutListingPlacements('posts', input.screens)
580
+ });
581
+ }
582
+ return listings;
583
+ }
584
+ /**
585
+ * A proposed product's description with each sentence that leaves a gap for
586
+ * the owner ("Delivery details: [how and when the card is sent].") taken
587
+ * out (AGL-3676): a start's products are listed as soon as they are written,
588
+ * and a live run's gift card showed its gaps on the product page. A line
589
+ * break is kept; nothing else is changed.
590
+ */ export function aiSiteProductDescriptionWithoutGaps(description) {
591
+ return description.split('\n').map((line)=>line.split(/(?<=[.!?])\s+/).filter((sentence)=>!/\[[^\]]+\]/.test(sentence)).join(' ')).join('\n').replace(/\n{3,}/g, '\n\n').trim();
592
+ }
593
+ /** What each product a start wrote says on the job's page (AGL-3676). */ export const AI_SITE_PRODUCT_NOTE = 'On your store at a starting price. Set its real price — and your own photo — in Products.';
594
+ /** The note a store's products row carries once they are written (AGL-3676): the step left before it sells. */ export const AI_SITE_PRODUCTS_PRICE_NOTE = 'Your products are on your store at a starting price. Set their real prices in Products.';
422
595
  /** What a page of a blog or a store is told about the posts or products built before it. */ export function aiSiteContentBriefLines(outputs) {
423
596
  const posts = outputs.filter((output)=>output.resource === 'entry');
424
597
  const products = outputs.filter((output)=>output.resource === 'product' && !output.proposal);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-content.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n publishContentEntries,\n writeContentCollection,\n writeContentEntryDraft,\n} from '@aglyn/tenant-data-admin/server/content-entry-drafts'\nimport { aiProductsProposalOf } from '../model/ai-products'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiSiteKindOfInputs } from '../model/ai-site-kinds'\nimport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport {\n AI_BLOG_POST_MAX_TOKENS,\n AI_BLOG_POST_STEP,\n generateAiBlogPost,\n} from '../runtime/ai-blog-post-generation'\nimport { aiLayoutStarterPhotos } from '../layout-language/ai-layout-pictures'\nimport { aiJobAdmissionFor, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * A GUIDED START'S FIRST POSTS AND FIRST PRODUCTS (AGL-3676).\n *\n * A site of the kind \"Blog & writing\" or \"Online store\" got the right look\n * and pages, and nothing real in them: the writing page's cards and the\n * shop's grid named posts and products nobody had written. On a paid plan,\n * the scaffold now builds one more part for those two kinds, before the\n * pages, each its own row with its own timer and credits:\n *\n * - WRITING YOUR FIRST POSTS: a content collection \"Blog\" — the platform's\n * own blog, served at `/{slug}` with an entry page per post — and\n * `AI_SITE_POSTS` posts in it, one generation a pass. Each is written as a\n * draft through the same rules the console's routes hold\n * (`content-entry-drafts.ts`), bylined with the business's name the person\n * gave, and given a starter photo as its cover. They go live with the\n * site: the guided start's publish publishes them with its pages\n * (`aiPublishSitePosts`), as a person's Publish would.\n * - ADDING YOUR FIRST PRODUCTS: the `products` step's catalog, asked for\n * `AI_SITE_PRODUCTS` products, each written through the commerce plugin's\n * `product` draft writer — a DRAFT with no price, off the storefront until\n * the owner prices it, since no price is ever invented. No photo either:\n * the writer takes none, and a stock picture of something else would show\n * a shopper a product that is not the one for sale.\n *\n * The pages built after them are told their titles or names, so the writing\n * page and the shop feature what exists rather than inventing their own.\n *\n * ── Not on the Free taste ────────────────────────────────────────────────\n *\n * A guided start's Free site, built on an empty site with its layout and\n * form, leaves 18 credits once its eight sections and its retried page's\n * room are held (`aiFreeSiteSectionsWithin`, AGL-3660), under the 23 one post\n * costs at its worst; and a Free plan includes neither\n * the `commerce` feature nor a product (`productsPerHost` is 0).\n * `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages\n * their kind already describes; the owner adds posts and products after.\n */\n\n/** How many first posts a paid blog gets. */\nexport const AI_SITE_POSTS = 3\n\n/** How many first products a paid store gets: asked for, and the most written. */\nexport const AI_SITE_PRODUCTS = { min: 3, max: 6 } as const\n\n/** What each part's row reads. */\nexport const AI_SITE_POSTS_LABEL = 'Writing your first posts'\nexport const AI_SITE_PRODUCTS_LABEL = 'Adding your first products'\n\n/** The blog the posts go in, and the addresses it may answer at (`model/ai-site-job.ts`). */\nexport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\n\n/** The input a site unit's own job carries what the part needs in. */\nexport const AI_SITE_CONTENT_INPUT = 'siteContent'\n\n/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */\nexport function aiSiteContentPart(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n freeTaste: boolean,\n): 'posts' | 'products' | null {\n if (freeTaste) return null\n const kind = aiSiteKindOfInputs(inputs)?.id\n if (kind === 'blog') return 'posts'\n if (kind === 'store') return 'products'\n return null\n}\n\n/**\n * ASSUMED: what one post pass reads and writes beside its generation: the\n * host, the collection's slug claim and the entry's transaction.\n */\nexport const AI_SITE_POST_WRITES_MS = 1_500\n\n/** One post's pass: its generation and re-ask at the `copy.blog` ceiling, and its writes. */\nexport const AI_SITE_POST_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS[AI_BLOG_POST_STEP],\n maxTokens: AI_BLOG_POST_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_SITE_POST_WRITES_MS,\n})\n\n/** What a posts unit's job is told: the posts already written and the addresses the blog must not take. */\nexport interface AiSitePostsInput {\n written: Array<{ id: string; title: string }>\n total: number\n /** Addresses the site's own pages answer at, which the blog must not shadow. */\n avoidSlugs: string[]\n /** Who the posts are by: the business's name as given, else empty. */\n byline: string\n}\n\n/** What a products unit's job is told. */\nexport interface AiSiteProductsInput {\n min: number\n max: number\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nfunction postsInputOf(job: Pick<AiJob, 'inputs'>): AiSitePostsInput {\n const raw = (job.inputs?.[AI_SITE_CONTENT_INPUT] ?? {}) as Partial<AiSitePostsInput>\n return {\n written: Array.isArray(raw.written) ? raw.written.filter((one) => one && typeof one.id === 'string') : [],\n total: typeof raw.total === 'number' && raw.total > 0 ? Math.floor(raw.total) : AI_SITE_POSTS,\n avoidSlugs: Array.isArray(raw.avoidSlugs) ? raw.avoidSlugs.map(str).filter(Boolean) : [],\n byline: str(raw.byline),\n }\n}\n\n/** The id of the `index`th post, of the blog its unit writes under `unitJobId`. */\nexport function aiSitePostId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** The id of the `index`th product a products unit writes. */\nexport function aiSiteProductId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** A site's subdomain and name, as its outputs and its posts' byline read them. */\nasync function readSite(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<{ subdomain: string | null; name: string } | null> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const seo = (host.get('seo') ?? {}) as Record<string, unknown>\n const subdomain = str(host.get('subdomain')) || null\n return { subdomain, name: str(host.get('displayName')) || str(seo['title']) }\n}\n\nconst limitReview = (message: string) => ({ reason: 'limit' as const, message, findings: [] })\n\nexport interface AiSitePostsRunnerDeps {\n generate?: typeof generateAiBlogPost\n writeCollection?: typeof writeContentCollection\n writeEntry?: typeof writeContentEntryDraft\n}\n\n/**\n * The posts unit's runner, on the job the scaffold derived for it: one post a\n * pass, continuing while posts remain. Its first pass makes the blog; every\n * pass finds it again under the unit's own id, and a post under its own id,\n * so a pass repeated after its write reports the post it already wrote.\n */\nexport function createAiSitePostsRunner(deps: AiSitePostsRunnerDeps = {}): AiJobStepRunner {\n const generate = deps.generate ?? generateAiBlogPost\n const writeCollection = deps.writeCollection ?? writeContentCollection\n const writeEntry = deps.writeEntry ?? writeContentEntryDraft\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, signal } = context\n const model = context.modelFor?.(AI_BLOG_POST_STEP) ?? aiModelForStep(AI_BLOG_POST_STEP)\n if (!job.hostId) return { ...aiUnspentOutcome(model), failure: 'Open the site before writing its posts.' }\n const input = postsInputOf(job)\n const index = input.written.length\n if (index >= input.total) return aiUnspentOutcome(model)\n const site = await readSite(firestore, job.hostId)\n if (!site) return { ...aiUnspentOutcome(model), failure: 'This site no longer exists.' }\n const avoid = new Set(input.avoidSlugs.map((slug) => slug.toLowerCase()))\n const blog = await writeCollection(firestore, {\n hostId: job.hostId,\n uid: job.createdBy,\n id: job.$id,\n displayName: AI_SITE_BLOG_NAME,\n slugs: AI_SITE_BLOG_SLUGS.filter((slug) => !avoid.has(slug)),\n now: context.now,\n })\n if (blog.ok === false) return aiUnspentOutcome(model, { review: limitReview(blog.error) })\n\n const generation = await generate({\n brief: job.brief,\n merchantWords: job.brief,\n earlierTitles: input.written.map((post) => post.title),\n index: index + 1,\n total: input.total,\n model,\n maxTokens: AI_SITE_POST_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const post = generation.value\n // A starter photo as its cover, turned by the job so blogs differ.\n const covers = aiLayoutStarterPhotos(\n Array.from({ length: input.total }, (_, slot) => ({\n imageId: `cover${slot}`,\n frameId: null,\n iconId: null,\n alt: '',\n aspect: 16 / 9,\n sectionIndex: slot + 1,\n role: 'gallery' as const,\n })),\n job.$id,\n )\n const byline = input.byline || site.name\n const written = await writeEntry(firestore, {\n hostId: job.hostId,\n collectionId: blog.id,\n uid: job.createdBy,\n id: aiSitePostId(job.$id, index),\n content: {\n title: post.title,\n excerpt: post.excerpt,\n body: post.body,\n seoDescription: post.seoDescription,\n coverImage: covers[index]?.src,\n ...(byline ? { authorName: byline } : {}),\n },\n now: context.now,\n })\n if (written.ok === false) return { ...spent, review: limitReview(written.error) }\n const output: AiJobOutput = {\n resource: 'entry',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: site.subdomain,\n label: written.title,\n note: `A post in ${blog.displayName}, at /${blog.slug}/${written.slug}. It goes live with the site.`,\n proposal: { collectionId: blog.id, collectionSlug: blog.slug, slug: written.slug },\n }\n return { ...spent, outputs: [output], ...(index + 1 < input.total ? { continue: true } : {}) }\n }\n}\n\nexport const runAiSitePostsUnit = createAiSitePostsRunner()\n\nexport interface AiSiteProductsRunnerDeps {\n /** The catalog's runner: the `products` step's. */\n catalog: AiJobStepRunner\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/** The sentence the products unit's brief ends with, which the catalog's rules read as how many. */\nexport function aiSiteProductsBriefLine(input: AiSiteProductsInput = AI_SITE_PRODUCTS): string {\n return `Propose between ${input.min} and ${input.max} products: the store's first ones.`\n}\n\n/**\n * The products unit's runner: the `products` step's catalog for the store's\n * brief, then each proposed product, up to the most a site starts with,\n * written as an unpriced draft by the plugin that keeps products. A product\n * past the plan's allowance stops the writing there, and the row says how\n * many were added; none at all is the allowance's review.\n */\nexport function createAiSiteProductsRunner(deps: AiSiteProductsRunnerDeps): AiJobStepRunner {\n const writerFor = deps.writerFor ?? pluginResourceDraftWriter\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n const keeper = writerFor('product')\n const model = context.modelFor?.('job.products') ?? aiModelForStep('job.products')\n if (!keeper || !job.hostId) return { ...aiUnspentOutcome(model), failure: 'Products are not available on this site.' }\n const outcome = await deps.catalog({\n ...context,\n job: { ...job, kind: 'products', inputs: { ...job.inputs, target: 'catalog' } },\n })\n if (outcome.refused || outcome.failure || outcome.review) return outcome\n const catalog = outcome.outputs.map((output) => ({ output, proposal: aiProductsProposalOf(output) })).find(\n (entry) => entry.proposal?.kind === 'catalog',\n )\n if (!catalog || catalog.proposal?.kind !== 'catalog') return { ...outcome, outputs: [] }\n const proposed = catalog.proposal.products.slice(0, AI_SITE_PRODUCTS.max)\n const outputs: AiJobOutput[] = []\n let stopped: string | null = null\n for (const [index, product] of proposed.entries()) {\n const written = await keeper.writer.write({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n id: aiSiteProductId(job.$id, index),\n name: product.name,\n // No price: the owner sets it. No photo: the writer takes none.\n content: {\n name: product.name,\n type: product.type,\n description: product.description,\n tags: product.tags,\n options: product.options,\n seoTitle: product.seoTitle,\n seoDescription: product.seoDescription,\n },\n })\n if (written.ok === false) {\n stopped = written.error\n break\n }\n outputs.push({\n resource: 'product',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: catalog.output.hostSubdomain ?? null,\n label: written.name,\n note: 'A draft with no price. Set its price and photo in Products to put it on sale.',\n })\n }\n if (!outputs.length) {\n return { ...outcome, outputs: [], review: limitReview(stopped ?? 'No product could be added.') }\n }\n // The allowance that stopped the rest is said where the last one added is.\n if (stopped) {\n const last = outputs[outputs.length - 1]\n last.note = `${last.note} ${outputs.length} of ${proposed.length} added: ${stopped}.`\n }\n return { ...outcome, outputs }\n }\n}\n\n/**\n * Whether a part may be built for this member on this site, asked before its\n * first pass so a refusal spends nothing; `null` admits. Products ask what a\n * `products` job asks (the plan's commerce feature, Commerce on and released\n * for the site) and the writer's own refusal (role, allowance); posts ask\n * that the member may write the site's content.\n */\nexport async function aiSiteContentRefusal(\n part: 'posts' | 'products',\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy' | 'inputs'> },\n deps: { writerFor?: typeof pluginResourceDraftWriter; admissionFor?: typeof aiJobAdmissionFor } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (part === 'posts') {\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n }\n const admission = (deps.admissionFor ?? aiJobAdmissionFor)('products')\n const refused: AiJobAdmissionRefusal | null = admission\n ? await admission({\n firestore,\n orgId: job.orgId,\n hostId: job.hostId,\n inputs: { target: 'catalog' },\n org: context.org ?? null,\n uid: job.createdBy,\n })\n : { status: 403, error: 'Products are not available on this site.' }\n if (refused) return refused.error\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)('product')\n if (!keeper) return 'Products are not available on this site.'\n const room = await keeper.writer.refusal({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n })\n return room?.error ?? null\n}\n\n/** The guided start's posts, published with its pages (AGL-3676). */\nexport interface AiSitePostsPublish {\n published: number\n kept: number\n /** The blog's own addresses, for the publish's cache drop. */\n paths: string[]\n}\n\n/**\n * Publishes the posts a guided start wrote, as the entry editor's Publish\n * does, once its pages are live. Never throws: a post that stays a draft is\n * the owner's to publish from Content.\n */\nexport async function aiPublishSitePosts(\n firestore: FirebaseFirestore.Firestore,\n input: { job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'createdBy'>; outputs: readonly AiJobOutput[]; now: Date },\n publish: typeof publishContentEntries = publishContentEntries,\n): Promise<AiSitePostsPublish | null> {\n const posts = input.outputs.filter((output) => output.resource === 'entry')\n const collectionId = str(posts[0]?.proposal?.['collectionId'])\n const collectionSlug = str(posts[0]?.proposal?.['collectionSlug'])\n if (!input.job.hostId || !posts.length || !collectionId) return null\n try {\n const result = await publish(firestore, {\n hostId: input.job.hostId,\n collectionId,\n uid: input.job.createdBy,\n ids: posts.map((post) => post.id),\n now: input.now,\n })\n if ('ok' in result) {\n console.warn('ai site posts not published', { jobId: input.job.$id, error: result.error })\n return { published: 0, kept: posts.length, paths: [] }\n }\n const slugs = posts\n .filter((post) => result.published.includes(post.id))\n .map((post) => str(post.proposal?.['slug']))\n .filter(Boolean)\n return {\n published: result.published.length,\n kept: result.kept.length,\n paths: collectionSlug ? [`/${collectionSlug}`, ...slugs.map((slug) => `/${collectionSlug}/${slug}`)] : [],\n }\n } catch (error) {\n console.error('ai site posts publish threw', { orgId: input.job.orgId, jobId: input.job.$id, error })\n return null\n }\n}\n\n/** What a page of a blog or a store is told about the posts or products built before it. */\nexport function aiSiteContentBriefLines(outputs: readonly AiJobOutput[]): string[] {\n const posts = outputs.filter((output) => output.resource === 'entry')\n const products = outputs.filter((output) => output.resource === 'product' && !output.proposal)\n const lines: string[] = []\n if (posts.length) {\n const slug = str(posts[0].proposal?.['collectionSlug'])\n lines.push(\n `This site's blog${slug ? ` at /${slug}` : ''} has these posts: ${posts.map((post) => `“${post.label}”`).join(', ')}.`,\n 'Where a page features writing, feature these posts by their titles; never name a post that is not in this list.',\n )\n }\n if (products.length) {\n lines.push(\n `This store's products are: ${products.map((product) => `“${product.label}”`).join(', ')}.`,\n 'Where a page features products, feature these by their names; never name another product, and never state a price.',\n )\n }\n return lines\n}\n"],"names":["hostRoleCanWrite","pluginResourceDraftWriter","publishContentEntries","writeContentCollection","writeContentEntryDraft","aiProductsProposalOf","aiSiteKindOfInputs","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","AI_STEP_TIERS","aiModelForStep","AI_BLOG_POST_MAX_TOKENS","AI_BLOG_POST_STEP","generateAiBlogPost","aiLayoutStarterPhotos","aiJobAdmissionFor","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_SITE_POSTS","AI_SITE_PRODUCTS","min","max","AI_SITE_POSTS_LABEL","AI_SITE_PRODUCTS_LABEL","AI_SITE_CONTENT_INPUT","aiSiteContentPart","inputs","freeTaste","kind","id","AI_SITE_POST_WRITES_MS","AI_SITE_POST_BUDGET","tier","maxTokens","lookups","ownReadsMs","str","value","trim","postsInputOf","job","raw","written","Array","isArray","filter","one","total","Math","floor","avoidSlugs","map","Boolean","byline","aiSitePostId","unitJobId","index","aiSiteProductId","readSite","firestore","hostId","host","collection","doc","get","exists","seo","subdomain","name","limitReview","message","reason","findings","createAiSitePostsRunner","deps","generate","writeCollection","writeEntry","context","covers","signal","model","modelFor","failure","input","length","site","avoid","Set","slug","toLowerCase","blog","uid","createdBy","$id","displayName","slugs","has","now","ok","review","error","generation","brief","merchantWords","earlierTitles","post","title","spent","status","refused","from","_","slot","imageId","frameId","iconId","alt","aspect","sectionIndex","role","collectionId","content","excerpt","body","seoDescription","coverImage","src","authorName","output","resource","hostSubdomain","label","note","proposal","collectionSlug","outputs","continue","runAiSitePostsUnit","aiSiteProductsBriefLine","createAiSiteProductsRunner","writerFor","catalog","keeper","outcome","target","find","entry","proposed","products","slice","stopped","product","entries","writer","write","orgId","org","type","description","tags","options","seoTitle","push","last","aiSiteContentRefusal","part","admission","admissionFor","room","refusal","aiPublishSitePosts","publish","posts","result","ids","console","warn","jobId","published","kept","paths","includes","aiSiteContentBriefLines","lines","join"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-content.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n publishContentEntries,\n writeContentCollection,\n writeContentEntryDraft,\n} from '@aglyn/tenant-data-admin/server/content-entry-drafts'\nimport { aiProductsProposalOf } from '../model/ai-products'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiSiteKindOfInputs } from '../model/ai-site-kinds'\nimport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS, aiSiteWords } from '../model/ai-site-job'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport {\n AI_BLOG_POST_MAX_TOKENS,\n AI_BLOG_POST_STEP,\n generateAiBlogPost,\n} from '../runtime/ai-blog-post-generation'\nimport {\n aiLayoutStarterPhotos,\n type AiLayoutPicturePhoto,\n type AiLayoutPictureSlot,\n} from '../layout-language/ai-layout-pictures'\nimport { aiLayoutStockPhotoSource } from './ai-layout-stock-photos'\nimport {\n aiLayoutListingId,\n aiLayoutListingPlacements,\n type AiLayoutListing,\n type AiLayoutListingScreen,\n} from '../layout-language/ai-layout-listings'\nimport { aiJobAdmissionFor, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * A GUIDED START'S FIRST POSTS AND FIRST PRODUCTS (AGL-3676).\n *\n * A site of the kind \"Blog & writing\" or \"Online store\" got the right look\n * and pages, and nothing real in them: the writing page's cards and the\n * shop's grid named posts and products nobody had written. On a paid plan,\n * the scaffold now builds one more part for those two kinds, before the\n * pages, each its own row with its own timer and credits:\n *\n * - WRITING YOUR FIRST POSTS: a content collection \"Blog\" — the platform's\n * own blog, served at `/{slug}` with an entry page per post — and\n * `AI_SITE_POSTS` posts in it, one generation a pass. Each is written as a\n * draft through the same rules the console's routes hold\n * (`content-entry-drafts.ts`), bylined with the business's name the person\n * gave, and given a cover: a stock photo of its own title where the\n * deployment has a library, else a starter (AGL-3676), which the blog's\n * cards show. They go live with the\n * site: the guided start's publish publishes them with its pages\n * (`aiPublishSitePosts`), as a person's Publish would.\n * - ADDING YOUR FIRST PRODUCTS: the `products` step's catalog, asked for\n * `AI_SITE_PRODUCTS` products, each written through the commerce plugin's\n * `product` draft writer. The catalog proposes no price, so each takes\n * the writer's default price (Zach, 2026-10-08: \"default a price\",\n * AGL-3676) for the owner to change; the job's row says so. Since the\n * 2026-10-08 Ember & Wick start, whose store showed no product at all,\n * each is LISTED at once (`comingSoon`: written `active`). Each gets a\n * photo in its slot, a stock photo of its own words where the deployment\n * has a library, else a starter, for the owner to replace with their\n * own.\n *\n * The pages built after them are told their titles or names, and the\n * sections that show them list the real records (`aiSiteListings`,\n * `ai-layout-listings.ts`): the shop's Product grid, the blog's posts.\n *\n * ── Not on the Free taste ────────────────────────────────────────────────\n *\n * A guided start's Free site, built on an empty site with its layout and\n * form, leaves 18 credits once its eight sections and its retried page's\n * room are held (`aiFreeSiteSectionsWithin`, AGL-3660), under the 23 one post\n * costs at its worst; and a Free plan includes neither\n * the `commerce` feature nor a product (`productsPerHost` is 0).\n * `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages\n * their kind already describes; the owner adds posts and products after.\n */\n\n/** How many first posts a paid blog gets. */\nexport const AI_SITE_POSTS = 3\n\n/** How many first products a paid store gets: asked for, and the most written. */\nexport const AI_SITE_PRODUCTS = { min: 3, max: 6 } as const\n\n/** What each part's row reads. */\nexport const AI_SITE_POSTS_LABEL = 'Writing your first posts'\nexport const AI_SITE_PRODUCTS_LABEL = 'Adding your first products'\n\n/** The blog the posts go in, and the addresses it may answer at (`model/ai-site-job.ts`). */\nexport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\n\n/** The input a site unit's own job carries what the part needs in. */\nexport const AI_SITE_CONTENT_INPUT = 'siteContent'\n\n/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */\nexport function aiSiteContentPart(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n freeTaste: boolean,\n): 'posts' | 'products' | null {\n if (freeTaste) return null\n const kind = aiSiteKindOfInputs(inputs)?.id\n if (kind === 'blog') return 'posts'\n if (kind === 'store') return 'products'\n return null\n}\n\n/**\n * ASSUMED: what one post pass reads and writes beside its generation: the\n * host, the collection's slug claim and the entry's transaction.\n */\nexport const AI_SITE_POST_WRITES_MS = 1_500\n\n/** One post's pass: its generation and re-ask at the `copy.blog` ceiling, and its writes. */\nexport const AI_SITE_POST_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS[AI_BLOG_POST_STEP],\n maxTokens: AI_BLOG_POST_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_SITE_POST_WRITES_MS,\n})\n\n/** What a posts unit's job is told: the posts already written and the addresses the blog must not take. */\nexport interface AiSitePostsInput {\n written: Array<{ id: string; title: string }>\n total: number\n /** Addresses the site's own pages answer at, which the blog must not shadow. */\n avoidSlugs: string[]\n /** Who the posts are by: the business's name as given, else empty. */\n byline: string\n}\n\n/** What a products unit's job is told. */\nexport interface AiSiteProductsInput {\n min: number\n max: number\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nfunction postsInputOf(job: Pick<AiJob, 'inputs'>): AiSitePostsInput {\n const raw = (job.inputs?.[AI_SITE_CONTENT_INPUT] ?? {}) as Partial<AiSitePostsInput>\n return {\n written: Array.isArray(raw.written) ? raw.written.filter((one) => one && typeof one.id === 'string') : [],\n total: typeof raw.total === 'number' && raw.total > 0 ? Math.floor(raw.total) : AI_SITE_POSTS,\n avoidSlugs: Array.isArray(raw.avoidSlugs) ? raw.avoidSlugs.map(str).filter(Boolean) : [],\n byline: str(raw.byline),\n }\n}\n\n/** The id of the `index`th post, of the blog its unit writes under `unitJobId`. */\nexport function aiSitePostId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** The id of the `index`th product a products unit writes. */\nexport function aiSiteProductId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** A site's subdomain and name, as its outputs and its posts' byline read them. */\nasync function readSite(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<{ subdomain: string | null; name: string } | null> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const seo = (host.get('seo') ?? {}) as Record<string, unknown>\n const subdomain = str(host.get('subdomain')) || null\n return { subdomain, name: str(host.get('displayName')) || str(seo['title']) }\n}\n\nconst limitReview = (message: string) => ({ reason: 'limit' as const, message, findings: [] })\n\nexport interface AiSitePostsRunnerDeps {\n generate?: typeof generateAiBlogPost\n writeCollection?: typeof writeContentCollection\n writeEntry?: typeof writeContentEntryDraft\n /** Where each post's cover comes from; the stock library, else the starters, otherwise. */\n cover?: typeof aiSitePostCover\n}\n\n/** A post's cover's shape: the blog's cards' (`COLLECTION_LIST_COVER_RATIO`, the posts listing's). */\nconst AI_SITE_POST_COVER_ASPECT = 3 / 2\n\n/**\n * A post's cover (AGL-3676): a stock photo of the post's own title, copied\n * into the site's library, where the deployment has a stock library; else the\n * starter photo a post was always given, turned by the job so blogs differ.\n * The blog's cards and the post's page show it, for the owner to replace.\n * Never throws: a post whose search fails takes its starter.\n */\nexport async function aiSitePostCover(input: {\n job: Pick<AiJob, '$id' | 'hostId' | 'createdBy' | 'inputs' | 'brief'>\n title: string\n index: number\n total: number\n signal?: AbortSignal\n stockPhotos?: typeof aiLayoutStockPhotoSource\n}): Promise<string | null> {\n const { job, index } = input\n if (job.hostId && input.title.trim()) {\n try {\n const source = (input.stockPhotos ?? aiLayoutStockPhotoSource)({\n hostId: job.hostId,\n uid: job.createdBy,\n seed: `${job.$id}:posts:${index}`,\n business: aiSiteWords(job.inputs).about || job.brief,\n sectionNames: ['Blog'],\n ...(input.signal ? { signal: input.signal } : {}),\n })\n const [found] = source\n ? await source([\n {\n imageId: `cover${index}`,\n frameId: null,\n iconId: null,\n alt: input.title,\n aspect: AI_SITE_POST_COVER_ASPECT,\n sectionIndex: 0,\n role: 'gallery' as const,\n },\n ])\n : []\n if (found?.src) return found.src\n } catch (error) {\n console.warn('ai site posts: the stock photo failed; the starter covers it', { error: String(error) })\n }\n }\n const starters = aiLayoutStarterPhotos(\n Array.from({ length: Math.max(input.total, index + 1) }, (_, slot) => ({\n imageId: `cover${slot}`,\n frameId: null,\n iconId: null,\n alt: '',\n aspect: 16 / 9,\n sectionIndex: slot + 1,\n role: 'gallery' as const,\n })),\n job.$id,\n )\n return starters[index]?.src ?? null\n}\n\n/**\n * The posts unit's runner, on the job the scaffold derived for it: one post a\n * pass, continuing while posts remain. Its first pass makes the blog; every\n * pass finds it again under the unit's own id, and a post under its own id,\n * so a pass repeated after its write reports the post it already wrote.\n */\nexport function createAiSitePostsRunner(deps: AiSitePostsRunnerDeps = {}): AiJobStepRunner {\n const generate = deps.generate ?? generateAiBlogPost\n const writeCollection = deps.writeCollection ?? writeContentCollection\n const writeEntry = deps.writeEntry ?? writeContentEntryDraft\n const coverFor = deps.cover ?? aiSitePostCover\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, signal } = context\n const model = context.modelFor?.(AI_BLOG_POST_STEP) ?? aiModelForStep(AI_BLOG_POST_STEP)\n if (!job.hostId) return { ...aiUnspentOutcome(model), failure: 'Open the site before writing its posts.' }\n const input = postsInputOf(job)\n const index = input.written.length\n if (index >= input.total) return aiUnspentOutcome(model)\n const site = await readSite(firestore, job.hostId)\n if (!site) return { ...aiUnspentOutcome(model), failure: 'This site no longer exists.' }\n const avoid = new Set(input.avoidSlugs.map((slug) => slug.toLowerCase()))\n const blog = await writeCollection(firestore, {\n hostId: job.hostId,\n uid: job.createdBy,\n id: job.$id,\n displayName: AI_SITE_BLOG_NAME,\n slugs: AI_SITE_BLOG_SLUGS.filter((slug) => !avoid.has(slug)),\n now: context.now,\n })\n if (blog.ok === false) return aiUnspentOutcome(model, { review: limitReview(blog.error) })\n\n const generation = await generate({\n brief: job.brief,\n merchantWords: job.brief,\n earlierTitles: input.written.map((post) => post.title),\n index: index + 1,\n total: input.total,\n model,\n maxTokens: AI_SITE_POST_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const post = generation.value\n // A stock photo of its title as its cover, else a starter (AGL-3676).\n const cover = await coverFor({\n job,\n title: post.title,\n index,\n total: input.total,\n ...(signal ? { signal } : {}),\n }).catch(() => null)\n const byline = input.byline || site.name\n const written = await writeEntry(firestore, {\n hostId: job.hostId,\n collectionId: blog.id,\n uid: job.createdBy,\n id: aiSitePostId(job.$id, index),\n content: {\n title: post.title,\n excerpt: post.excerpt,\n body: post.body,\n seoDescription: post.seoDescription,\n ...(cover ? { coverImage: cover, coverImageAlt: post.title } : {}),\n ...(byline ? { authorName: byline } : {}),\n },\n now: context.now,\n })\n if (written.ok === false) return { ...spent, review: limitReview(written.error) }\n const output: AiJobOutput = {\n resource: 'entry',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: site.subdomain,\n label: written.title,\n note: `A post in ${blog.displayName}, at /${blog.slug}/${written.slug}. It goes live with the site.`,\n proposal: { collectionId: blog.id, collectionSlug: blog.slug, slug: written.slug },\n }\n return { ...spent, outputs: [output], ...(index + 1 < input.total ? { continue: true } : {}) }\n }\n}\n\nexport const runAiSitePostsUnit = createAiSitePostsRunner()\n\nexport interface AiSiteProductsRunnerDeps {\n /** The catalog's runner: the `products` step's. */\n catalog: AiJobStepRunner\n writerFor?: typeof pluginResourceDraftWriter\n /** Where each product's photo comes from; the stock library, else the starters, otherwise. */\n photos?: typeof aiSiteProductPhotos\n}\n\n/**\n * A photo for each product (AGL-3676), in order: a stock photo of the\n * product's own words, copied into the site's library, where the deployment\n * has a stock library; else a starter photo, as a page's empty picture takes\n * one. It fills the photo slot every product card and product page has, for\n * the owner to replace with their own; the product's note says so. Never\n * throws: a product with no photo is still written.\n */\nexport async function aiSiteProductPhotos(input: {\n job: Pick<AiJob, '$id' | 'hostId' | 'createdBy' | 'inputs' | 'brief'>\n names: readonly string[]\n signal?: AbortSignal\n stockPhotos?: typeof aiLayoutStockPhotoSource\n}): Promise<Array<string | null>> {\n if (!input.names.length || !input.job.hostId) return input.names.map(() => null)\n const slots: AiLayoutPictureSlot[] = input.names.map((name, index) => ({\n imageId: `product${index}`,\n frameId: null,\n iconId: null,\n alt: name,\n // The product card's own shape (`cardStyle: 'photo'`).\n aspect: 4 / 5,\n sectionIndex: 0,\n role: 'gallery' as const,\n }))\n const seed = `${input.job.$id}:products`\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n try {\n const source = (input.stockPhotos ?? aiLayoutStockPhotoSource)({\n hostId: input.job.hostId,\n uid: input.job.createdBy,\n seed,\n business: aiSiteWords(input.job.inputs).about || input.job.brief,\n sectionNames: ['Products'],\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (source) found = await source(slots)\n } catch (error) {\n console.warn('ai site products: the stock photos failed; starter photos fill them', { error: String(error) })\n }\n const starters = aiLayoutStarterPhotos(slots, seed)\n return slots.map((_slot, index) => found[index]?.src ?? starters[index]?.src ?? null)\n}\n\n/** The sentence the products unit's brief ends with, which the catalog's rules read as how many. */\nexport function aiSiteProductsBriefLine(input: AiSiteProductsInput = AI_SITE_PRODUCTS): string {\n return `Propose between ${input.min} and ${input.max} products: the store's first ones.`\n}\n\n/**\n * The products unit's runner: the `products` step's catalog for the store's\n * brief, then each proposed product, up to the most a site starts with,\n * written as an unpriced draft by the plugin that keeps products. A product\n * past the plan's allowance stops the writing there, and the row says how\n * many were added; none at all is the allowance's review.\n */\nexport function createAiSiteProductsRunner(deps: AiSiteProductsRunnerDeps): AiJobStepRunner {\n const writerFor = deps.writerFor ?? pluginResourceDraftWriter\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n const keeper = writerFor('product')\n const model = context.modelFor?.('job.products') ?? aiModelForStep('job.products')\n if (!keeper || !job.hostId) return { ...aiUnspentOutcome(model), failure: 'Products are not available on this site.' }\n const outcome = await deps.catalog({\n ...context,\n job: { ...job, kind: 'products', inputs: { ...job.inputs, target: 'catalog' } },\n })\n if (outcome.refused || outcome.failure || outcome.review) return outcome\n const catalog = outcome.outputs.map((output) => ({ output, proposal: aiProductsProposalOf(output) })).find(\n (entry) => entry.proposal?.kind === 'catalog',\n )\n if (!catalog || catalog.proposal?.kind !== 'catalog') return { ...outcome, outputs: [] }\n const proposed = catalog.proposal.products.slice(0, AI_SITE_PRODUCTS.max)\n const photos = await (deps.photos ?? aiSiteProductPhotos)({\n job,\n names: proposed.map((product) => product.name),\n ...(context.signal ? { signal: context.signal } : {}),\n }).catch(() => proposed.map(() => null))\n const outputs: AiJobOutput[] = []\n let stopped: string | null = null\n for (const [index, product] of proposed.entries()) {\n const photo = photos[index]\n const written = await keeper.writer.write({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n id: aiSiteProductId(job.$id, index),\n name: product.name,\n // No price stated: the writer gives it the store's default price\n // for the owner to change (AGL-3676). Listed at once. Its photo\n // slot holds a stock or starter photo for the owner to replace.\n content: {\n name: product.name,\n type: product.type,\n // Listed at once, so no \"[gift card terms]\" gap reaches a shopper:\n // a sentence that leaves a fact for the owner is left out.\n description: aiSiteProductDescriptionWithoutGaps(product.description),\n tags: product.tags,\n options: product.options,\n seoTitle: product.seoTitle,\n seoDescription: product.seoDescription,\n comingSoon: true,\n ...(photo ? { mediaUrls: [photo] } : {}),\n },\n })\n if (written.ok === false) {\n stopped = written.error\n break\n }\n outputs.push({\n resource: 'product',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: catalog.output.hostSubdomain ?? null,\n label: written.name,\n note: AI_SITE_PRODUCT_NOTE,\n })\n }\n if (!outputs.length) {\n return { ...outcome, outputs: [], review: limitReview(stopped ?? 'No product could be added.') }\n }\n // The allowance that stopped the rest is said where the last one added is.\n if (stopped) {\n const last = outputs[outputs.length - 1]\n last.note = `${last.note} ${outputs.length} of ${proposed.length} added: ${stopped}.`\n }\n return { ...outcome, outputs }\n }\n}\n\n/**\n * Whether a part may be built for this member on this site, asked before its\n * first pass so a refusal spends nothing; `null` admits. Products ask what a\n * `products` job asks (the plan's commerce feature, Commerce on and released\n * for the site) and the writer's own refusal (role, allowance); posts ask\n * that the member may write the site's content.\n */\nexport async function aiSiteContentRefusal(\n part: 'posts' | 'products',\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy' | 'inputs'> },\n deps: { writerFor?: typeof pluginResourceDraftWriter; admissionFor?: typeof aiJobAdmissionFor } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (part === 'posts') {\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n }\n const admission = (deps.admissionFor ?? aiJobAdmissionFor)('products')\n const refused: AiJobAdmissionRefusal | null = admission\n ? await admission({\n firestore,\n orgId: job.orgId,\n hostId: job.hostId,\n inputs: { target: 'catalog' },\n org: context.org ?? null,\n uid: job.createdBy,\n })\n : { status: 403, error: 'Products are not available on this site.' }\n if (refused) return refused.error\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)('product')\n if (!keeper) return 'Products are not available on this site.'\n const room = await keeper.writer.refusal({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n })\n return room?.error ?? null\n}\n\n/** The guided start's posts, published with its pages (AGL-3676). */\nexport interface AiSitePostsPublish {\n published: number\n kept: number\n /** The blog's own addresses, for the publish's cache drop. */\n paths: string[]\n}\n\n/**\n * Publishes the posts a guided start wrote, as the entry editor's Publish\n * does, once its pages are live. Never throws: a post that stays a draft is\n * the owner's to publish from Content.\n */\nexport async function aiPublishSitePosts(\n firestore: FirebaseFirestore.Firestore,\n input: { job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'createdBy'>; outputs: readonly AiJobOutput[]; now: Date },\n publish: typeof publishContentEntries = publishContentEntries,\n): Promise<AiSitePostsPublish | null> {\n const posts = input.outputs.filter((output) => output.resource === 'entry')\n const collectionId = str(posts[0]?.proposal?.['collectionId'])\n const collectionSlug = str(posts[0]?.proposal?.['collectionSlug'])\n if (!input.job.hostId || !posts.length || !collectionId) return null\n try {\n const result = await publish(firestore, {\n hostId: input.job.hostId,\n collectionId,\n uid: input.job.createdBy,\n ids: posts.map((post) => post.id),\n now: input.now,\n })\n if ('ok' in result) {\n console.warn('ai site posts not published', { jobId: input.job.$id, error: result.error })\n return { published: 0, kept: posts.length, paths: [] }\n }\n const slugs = posts\n .filter((post) => result.published.includes(post.id))\n .map((post) => str(post.proposal?.['slug']))\n .filter(Boolean)\n return {\n published: result.published.length,\n kept: result.kept.length,\n paths: collectionSlug ? [`/${collectionSlug}`, ...slugs.map((slug) => `/${collectionSlug}/${slug}`)] : [],\n }\n } catch (error) {\n console.error('ai site posts publish threw', { orgId: input.job.orgId, jobId: input.job.$id, error })\n return null\n }\n}\n\n/**\n * The site's listings a layout or a page unit is handed (AGL-3676,\n * `ai-layout-listings.ts`): the store's catalog once its products are written,\n * the blog once its posts are, each with the sections of the plan's pages\n * that list it. A layout is built before either, so it is told the store will\n * sell (`sells`, its ledger still owing the products part), which puts the\n * cart in its header.\n */\nexport function aiSiteListings(input: {\n outputs: readonly AiJobOutput[]\n screens: readonly AiLayoutListingScreen[]\n /** The ledger owes a store's first products: a layout built before them carries the cart. */\n sells?: boolean\n}): AiLayoutListing[] {\n const listings: AiLayoutListing[] = []\n const products = input.outputs.filter((output) => output.resource === 'product' && !output.proposal)\n if (products.length || input.sells) {\n listings.push({\n id: aiLayoutListingId('products'),\n kind: 'products',\n name: 'the shop',\n records: products.map((product) => product.label),\n placements: products.length ? aiLayoutListingPlacements('products', input.screens) : [],\n })\n }\n const posts = input.outputs.filter((output) => output.resource === 'entry')\n const slug = str(posts[0]?.proposal?.['collectionSlug'])\n if (posts.length && slug) {\n listings.push({\n id: aiLayoutListingId('posts'),\n kind: 'posts',\n name: 'the blog',\n records: posts.map((post) => post.label),\n href: `/${slug}`,\n collectionSlug: slug,\n placements: aiLayoutListingPlacements('posts', input.screens),\n })\n }\n return listings\n}\n\n/**\n * A proposed product's description with each sentence that leaves a gap for\n * the owner (\"Delivery details: [how and when the card is sent].\") taken\n * out (AGL-3676): a start's products are listed as soon as they are written,\n * and a live run's gift card showed its gaps on the product page. A line\n * break is kept; nothing else is changed.\n */\nexport function aiSiteProductDescriptionWithoutGaps(description: string): string {\n return description\n .split('\\n')\n .map((line) =>\n line\n .split(/(?<=[.!?])\\s+/)\n .filter((sentence) => !/\\[[^\\]]+\\]/.test(sentence))\n .join(' '),\n )\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\n/** What each product a start wrote says on the job's page (AGL-3676). */\nexport const AI_SITE_PRODUCT_NOTE =\n 'On your store at a starting price. Set its real price — and your own photo — in Products.'\n\n/** The note a store's products row carries once they are written (AGL-3676): the step left before it sells. */\nexport const AI_SITE_PRODUCTS_PRICE_NOTE =\n 'Your products are on your store at a starting price. Set their real prices in Products.'\n\n/** What a page of a blog or a store is told about the posts or products built before it. */\nexport function aiSiteContentBriefLines(outputs: readonly AiJobOutput[]): string[] {\n const posts = outputs.filter((output) => output.resource === 'entry')\n const products = outputs.filter((output) => output.resource === 'product' && !output.proposal)\n const lines: string[] = []\n if (posts.length) {\n const slug = str(posts[0].proposal?.['collectionSlug'])\n lines.push(\n `This site's blog${slug ? ` at /${slug}` : ''} has these posts: ${posts.map((post) => `“${post.label}”`).join(', ')}.`,\n 'Where a page features writing, feature these posts by their titles; never name a post that is not in this list.',\n )\n }\n if (products.length) {\n lines.push(\n `This store's products are: ${products.map((product) => `“${product.label}”`).join(', ')}.`,\n 'Where a page features products, feature these by their names; never name another product, and never state a price.',\n )\n }\n return lines\n}\n"],"names":["hostRoleCanWrite","pluginResourceDraftWriter","publishContentEntries","writeContentCollection","writeContentEntryDraft","aiProductsProposalOf","aiSiteKindOfInputs","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","aiSiteWords","AI_STEP_TIERS","aiModelForStep","AI_BLOG_POST_MAX_TOKENS","AI_BLOG_POST_STEP","generateAiBlogPost","aiLayoutStarterPhotos","aiLayoutStockPhotoSource","aiLayoutListingId","aiLayoutListingPlacements","aiJobAdmissionFor","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_SITE_POSTS","AI_SITE_PRODUCTS","min","max","AI_SITE_POSTS_LABEL","AI_SITE_PRODUCTS_LABEL","AI_SITE_CONTENT_INPUT","aiSiteContentPart","inputs","freeTaste","kind","id","AI_SITE_POST_WRITES_MS","AI_SITE_POST_BUDGET","tier","maxTokens","lookups","ownReadsMs","str","value","trim","postsInputOf","job","raw","written","Array","isArray","filter","one","total","Math","floor","avoidSlugs","map","Boolean","byline","aiSitePostId","unitJobId","index","aiSiteProductId","readSite","firestore","hostId","host","collection","doc","get","exists","seo","subdomain","name","limitReview","message","reason","findings","AI_SITE_POST_COVER_ASPECT","aiSitePostCover","input","starters","title","source","stockPhotos","uid","createdBy","seed","$id","business","about","brief","sectionNames","signal","found","imageId","frameId","iconId","alt","aspect","sectionIndex","role","src","error","console","warn","String","from","length","_","slot","createAiSitePostsRunner","deps","generate","writeCollection","writeEntry","coverFor","cover","context","model","modelFor","failure","site","avoid","Set","slug","toLowerCase","blog","displayName","slugs","has","now","ok","review","generation","merchantWords","earlierTitles","post","spent","status","refused","catch","collectionId","content","excerpt","body","seoDescription","coverImage","coverImageAlt","authorName","output","resource","hostSubdomain","label","note","proposal","collectionSlug","outputs","continue","runAiSitePostsUnit","aiSiteProductPhotos","names","slots","_slot","aiSiteProductsBriefLine","createAiSiteProductsRunner","writerFor","catalog","keeper","outcome","target","find","entry","proposed","products","slice","photos","product","stopped","entries","photo","writer","write","orgId","org","type","description","aiSiteProductDescriptionWithoutGaps","tags","options","seoTitle","comingSoon","mediaUrls","push","AI_SITE_PRODUCT_NOTE","last","aiSiteContentRefusal","part","admission","admissionFor","room","refusal","aiPublishSitePosts","publish","posts","result","ids","jobId","published","kept","paths","includes","aiSiteListings","listings","sells","records","placements","screens","href","split","line","sentence","test","join","replace","AI_SITE_PRODUCTS_PRICE_NOTE","aiSiteContentBriefLines","lines"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,yBAAyB,QAAQ,qDAAoD;AAC9F,SACEC,qBAAqB,EACrBC,sBAAsB,EACtBC,sBAAsB,QACjB,uDAAsD;AAC7D,SAASC,oBAAoB,QAAQ,0BAAsB;AAE3D,SAASC,kBAAkB,QAAQ,4BAAwB;AAC3D,SAASC,iBAAiB,EAAEC,kBAAkB,EAAEC,WAAW,QAAQ,0BAAsB;AACzF,SAASC,aAAa,QAAQ,0BAAsB;AACpD,SAASC,cAAc,QAAQ,0BAAsB;AACrD,SACEC,uBAAuB,EACvBC,iBAAiB,EACjBC,kBAAkB,QACb,wCAAoC;AAC3C,SACEC,qBAAqB,QAGhB,2CAAuC;AAC9C,SAASC,wBAAwB,QAAQ,8BAA0B;AACnE,SACEC,iBAAiB,EACjBC,yBAAyB,QAGpB,2CAAuC;AAC9C,SAASC,iBAAiB,QAAoC,wBAAoB;AAClF,SAASC,eAAe,QAAQ,qBAAiB;AACjD,SAASC,iBAAiB,EAAEC,gBAAgB,QAAQ,yBAAqB;AAGzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GAED,2CAA2C,GAC3C,OAAO,MAAMC,gBAAgB,EAAC;AAE9B,gFAAgF,GAChF,OAAO,MAAMC,mBAAmB;IAAEC,KAAK;IAAGC,KAAK;AAAE,EAAU;AAE3D,gCAAgC,GAChC,OAAO,MAAMC,sBAAsB,2BAA0B;AAC7D,OAAO,MAAMC,yBAAyB,6BAA4B;AAElE,2FAA2F,GAC3F,SAASrB,iBAAiB,EAAEC,kBAAkB,QAAQ,0BAAsB;AAE5E,oEAAoE,GACpE,OAAO,MAAMqB,wBAAwB,cAAa;AAElD,6FAA6F,GAC7F,OAAO,SAASC,kBACdC,MAA4D,EAC5DC,SAAkB;QAGL1B;IADb,IAAI0B,WAAW,OAAO;IACtB,MAAMC,QAAO3B,sBAAAA,mBAAmByB,4BAAnBzB,oBAA4B4B,EAAE;IAC3C,IAAID,SAAS,QAAQ,OAAO;IAC5B,IAAIA,SAAS,SAAS,OAAO;IAC7B,OAAO;AACT;AAEA;;;CAGC,GACD,OAAO,MAAME,yBAAyB,KAAK;AAE3C,2FAA2F,GAC3F,OAAO,MAAMC,sBAAsBhB,gBAAgB;IACjDiB,MAAM3B,aAAa,CAACG,kBAAkB;IACtCyB,WAAW1B;IACX2B,SAAS;IACTC,YAAYL;AACd,GAAE;AAkBF,MAAMM,MAAM,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAEpF,SAASC,aAAaC,GAA0B;;QACjCA;IAAb,MAAMC,eAAOD,cAAAA,IAAId,MAAM,qBAAVc,WAAY,CAAChB,sBAAsB,mBAAI,CAAC;IACrD,OAAO;QACLkB,SAASC,MAAMC,OAAO,CAACH,IAAIC,OAAO,IAAID,IAAIC,OAAO,CAACG,MAAM,CAAC,CAACC,MAAQA,OAAO,OAAOA,IAAIjB,EAAE,KAAK,YAAY,EAAE;QACzGkB,OAAO,OAAON,IAAIM,KAAK,KAAK,YAAYN,IAAIM,KAAK,GAAG,IAAIC,KAAKC,KAAK,CAACR,IAAIM,KAAK,IAAI7B;QAChFgC,YAAYP,MAAMC,OAAO,CAACH,IAAIS,UAAU,IAAIT,IAAIS,UAAU,CAACC,GAAG,CAACf,KAAKS,MAAM,CAACO,WAAW,EAAE;QACxFC,QAAQjB,IAAIK,IAAIY,MAAM;IACxB;AACF;AAEA,iFAAiF,GACjF,OAAO,SAASC,aAAaC,SAAiB,EAAEC,KAAa;IAC3D,OAAO,GAAGD,UAAU,CAAC,EAAEC,OAAO;AAChC;AAEA,4DAA4D,GAC5D,OAAO,SAASC,gBAAgBF,SAAiB,EAAEC,KAAa;IAC9D,OAAO,GAAGD,UAAU,CAAC,EAAEC,OAAO;AAChC;AAEA,iFAAiF,GACjF,eAAeE,SACbC,SAAsC,EACtCC,MAAc;QAIDC;IAFb,MAAMA,OAAO,MAAMF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACH,QAAQI,GAAG;IAChE,IAAI,CAACH,KAAKI,MAAM,EAAE,OAAO;IACzB,MAAMC,OAAOL,YAAAA,KAAKG,GAAG,CAAC,kBAATH,YAAmB,CAAC;IACjC,MAAMM,YAAY/B,IAAIyB,KAAKG,GAAG,CAAC,iBAAiB;IAChD,OAAO;QAAEG;QAAWC,MAAMhC,IAAIyB,KAAKG,GAAG,CAAC,mBAAmB5B,IAAI8B,GAAG,CAAC,QAAQ;IAAE;AAC9E;AAEA,MAAMG,cAAc,CAACC,UAAqB,CAAA;QAAEC,QAAQ;QAAkBD;QAASE,UAAU,EAAE;IAAC,CAAA;AAU5F,oGAAoG,GACpG,MAAMC,4BAA4B,IAAI;AAEtC;;;;;;CAMC,GACD,OAAO,eAAeC,gBAAgBC,KAOrC;;QA0CQC;IAzCP,MAAM,EAAEpC,GAAG,EAAEgB,KAAK,EAAE,GAAGmB;IACvB,IAAInC,IAAIoB,MAAM,IAAIe,MAAME,KAAK,CAACvC,IAAI,IAAI;QACpC,IAAI;gBACcqC;YAAhB,MAAMG,SAAS,EAACH,qBAAAA,MAAMI,WAAW,YAAjBJ,qBAAqBhE,0BAA0B;gBAC7DiD,QAAQpB,IAAIoB,MAAM;gBAClBoB,KAAKxC,IAAIyC,SAAS;gBAClBC,MAAM,GAAG1C,IAAI2C,GAAG,CAAC,OAAO,EAAE3B,OAAO;gBACjC4B,UAAUhF,YAAYoC,IAAId,MAAM,EAAE2D,KAAK,IAAI7C,IAAI8C,KAAK;gBACpDC,cAAc;oBAAC;iBAAO;eAClBZ,MAAMa,MAAM,GAAG;gBAAEA,QAAQb,MAAMa,MAAM;YAAC,IAAI,CAAC;YAEjD,MAAM,CAACC,MAAM,GAAGX,SACZ,MAAMA,OAAO;gBACX;oBACEY,SAAS,CAAC,KAAK,EAAElC,OAAO;oBACxBmC,SAAS;oBACTC,QAAQ;oBACRC,KAAKlB,MAAME,KAAK;oBAChBiB,QAAQrB;oBACRsB,cAAc;oBACdC,MAAM;gBACR;aACD,IACD,EAAE;YACN,IAAIP,yBAAAA,MAAOQ,GAAG,EAAE,OAAOR,MAAMQ,GAAG;QAClC,EAAE,OAAOC,OAAO;YACdC,QAAQC,IAAI,CAAC,gEAAgE;gBAAEF,OAAOG,OAAOH;YAAO;QACtG;IACF;IACA,MAAMtB,WAAWlE,sBACfiC,MAAM2D,IAAI,CAAC;QAAEC,QAAQvD,KAAK3B,GAAG,CAACsD,MAAM5B,KAAK,EAAES,QAAQ;IAAG,GAAG,CAACgD,GAAGC,OAAU,CAAA;YACrEf,SAAS,CAAC,KAAK,EAAEe,MAAM;YACvBd,SAAS;YACTC,QAAQ;YACRC,KAAK;YACLC,QAAQ,KAAK;YACbC,cAAcU,OAAO;YACrBT,MAAM;QACR,CAAA,IACAxD,IAAI2C,GAAG;IAET,gBAAOP,kBAAAA,QAAQ,CAACpB,MAAM,qBAAfoB,gBAAiBqB,GAAG,mBAAI;AACjC;AAEA;;;;;CAKC,GACD,OAAO,SAASS,wBAAwBC,OAA8B,CAAC,CAAC;QACrDA,gBACOA,uBACLA,kBACFA;IAHjB,MAAMC,YAAWD,iBAAAA,KAAKC,QAAQ,YAAbD,iBAAiBlG;IAClC,MAAMoG,mBAAkBF,wBAAAA,KAAKE,eAAe,YAApBF,wBAAwB7G;IAChD,MAAMgH,cAAaH,mBAAAA,KAAKG,UAAU,YAAfH,mBAAmB5G;IACtC,MAAMgH,YAAWJ,cAAAA,KAAKK,KAAK,YAAVL,cAAcjC;IAC/B,OAAO,OAAOuC;;QACZ,MAAM,EAAEzE,GAAG,EAAEmB,SAAS,EAAE6B,MAAM,EAAE,GAAGyB;QACnC,MAAMC,gBAAQD,QAAQE,QAAQ,oBAAhBF,QAAQE,QAAQ,MAAhBF,SAAmBzG,qCAAsBF,eAAeE;QACtE,IAAI,CAACgC,IAAIoB,MAAM,EAAE,OAAO,aAAK3C,iBAAiBiG;YAAQE,SAAS;;QAC/D,MAAMzC,QAAQpC,aAAaC;QAC3B,MAAMgB,QAAQmB,MAAMjC,OAAO,CAAC6D,MAAM;QAClC,IAAI/C,SAASmB,MAAM5B,KAAK,EAAE,OAAO9B,iBAAiBiG;QAClD,MAAMG,OAAO,MAAM3D,SAASC,WAAWnB,IAAIoB,MAAM;QACjD,IAAI,CAACyD,MAAM,OAAO,aAAKpG,iBAAiBiG;YAAQE,SAAS;;QACzD,MAAME,QAAQ,IAAIC,IAAI5C,MAAMzB,UAAU,CAACC,GAAG,CAAC,CAACqE,OAASA,KAAKC,WAAW;QACrE,MAAMC,OAAO,MAAMb,gBAAgBlD,WAAW;YAC5CC,QAAQpB,IAAIoB,MAAM;YAClBoB,KAAKxC,IAAIyC,SAAS;YAClBpD,IAAIW,IAAI2C,GAAG;YACXwC,aAAazH;YACb0H,OAAOzH,mBAAmB0C,MAAM,CAAC,CAAC2E,OAAS,CAACF,MAAMO,GAAG,CAACL;YACtDM,KAAKb,QAAQa,GAAG;QAClB;QACA,IAAIJ,KAAKK,EAAE,KAAK,OAAO,OAAO9G,iBAAiBiG,OAAO;YAAEc,QAAQ3D,YAAYqD,KAAKxB,KAAK;QAAE;QAExF,MAAM+B,aAAa,MAAMrB,SAAS;YAChCtB,OAAO9C,IAAI8C,KAAK;YAChB4C,eAAe1F,IAAI8C,KAAK;YACxB6C,eAAexD,MAAMjC,OAAO,CAACS,GAAG,CAAC,CAACiF,OAASA,KAAKvD,KAAK;YACrDrB,OAAOA,QAAQ;YACfT,OAAO4B,MAAM5B,KAAK;YAClBmE;YACAjF,WAAWF,oBAAoBE,SAAS,CAACiF;WACrC1B,SAAS;YAAEA;QAAO,IAAI,CAAC;QAE7B,MAAM6C,QAAQrH,kBAAkBiH;QAChC,IAAIA,WAAWK,MAAM,KAAK,WAAW,OAAO,aAAKD;YAAOE,SAAS;;QACjE,IAAIN,WAAWK,MAAM,KAAK,eAAe,OAAO,aAAKD;YAAOjB,SAASa,WAAW3D,OAAO;;QACvF,MAAM8D,OAAOH,WAAW5F,KAAK;QAC7B,sEAAsE;QACtE,MAAM2E,QAAQ,MAAMD,SAAS;YAC3BvE;YACAqC,OAAOuD,KAAKvD,KAAK;YACjBrB;YACAT,OAAO4B,MAAM5B,KAAK;WACdyC,SAAS;YAAEA;QAAO,IAAI,CAAC,IAC1BgD,KAAK,CAAC,IAAM;QACf,MAAMnF,SAASsB,MAAMtB,MAAM,IAAIgE,KAAKjD,IAAI;QACxC,MAAM1B,UAAU,MAAMoE,WAAWnD,WAAW;YAC1CC,QAAQpB,IAAIoB,MAAM;YAClB6E,cAAcf,KAAK7F,EAAE;YACrBmD,KAAKxC,IAAIyC,SAAS;YAClBpD,IAAIyB,aAAad,IAAI2C,GAAG,EAAE3B;YAC1BkF,SAAS;gBACP7D,OAAOuD,KAAKvD,KAAK;gBACjB8D,SAASP,KAAKO,OAAO;gBACrBC,MAAMR,KAAKQ,IAAI;gBACfC,gBAAgBT,KAAKS,cAAc;eAC/B7B,QAAQ;gBAAE8B,YAAY9B;gBAAO+B,eAAeX,KAAKvD,KAAK;YAAC,IAAI,CAAC,GAC5DxB,SAAS;gBAAE2F,YAAY3F;YAAO,IAAI,CAAC;YAEzCyE,KAAKb,QAAQa,GAAG;QAClB;QACA,IAAIpF,QAAQqF,EAAE,KAAK,OAAO,OAAO,aAAKM;YAAOL,QAAQ3D,YAAY3B,QAAQwD,KAAK;;QAC9E,MAAM+C,SAAsB;YAC1BC,UAAU;YACVrH,IAAIa,QAAQb,EAAE;YACd+B,QAAQpB,IAAIoB,MAAM;YAClBuF,eAAe9B,KAAKlD,SAAS;YAC7BiF,OAAO1G,QAAQmC,KAAK;YACpBwE,MAAM,CAAC,UAAU,EAAE3B,KAAKC,WAAW,CAAC,MAAM,EAAED,KAAKF,IAAI,CAAC,CAAC,EAAE9E,QAAQ8E,IAAI,CAAC,6BAA6B,CAAC;YACpG8B,UAAU;gBAAEb,cAAcf,KAAK7F,EAAE;gBAAE0H,gBAAgB7B,KAAKF,IAAI;gBAAEA,MAAM9E,QAAQ8E,IAAI;YAAC;QACnF;QACA,OAAO,aAAKa;YAAOmB,SAAS;gBAACP;aAAO;WAAMzF,QAAQ,IAAImB,MAAM5B,KAAK,GAAG;YAAE0G,UAAU;QAAK,IAAI,CAAC;IAC5F;AACF;AAEA,OAAO,MAAMC,qBAAqBhD,0BAAyB;AAU3D;;;;;;;CAOC,GACD,OAAO,eAAeiD,oBAAoBhF,KAKzC;IACC,IAAI,CAACA,MAAMiF,KAAK,CAACrD,MAAM,IAAI,CAAC5B,MAAMnC,GAAG,CAACoB,MAAM,EAAE,OAAOe,MAAMiF,KAAK,CAACzG,GAAG,CAAC,IAAM;IAC3E,MAAM0G,QAA+BlF,MAAMiF,KAAK,CAACzG,GAAG,CAAC,CAACiB,MAAMZ,QAAW,CAAA;YACrEkC,SAAS,CAAC,OAAO,EAAElC,OAAO;YAC1BmC,SAAS;YACTC,QAAQ;YACRC,KAAKzB;YACL,uDAAuD;YACvD0B,QAAQ,IAAI;YACZC,cAAc;YACdC,MAAM;QACR,CAAA;IACA,MAAMd,OAAO,GAAGP,MAAMnC,GAAG,CAAC2C,GAAG,CAAC,SAAS,CAAC;IACxC,IAAIM,QAAoD,EAAE;IAC1D,IAAI;YACcd;QAAhB,MAAMG,SAAS,EAACH,qBAAAA,MAAMI,WAAW,YAAjBJ,qBAAqBhE,0BAA0B;YAC7DiD,QAAQe,MAAMnC,GAAG,CAACoB,MAAM;YACxBoB,KAAKL,MAAMnC,GAAG,CAACyC,SAAS;YACxBC;YACAE,UAAUhF,YAAYuE,MAAMnC,GAAG,CAACd,MAAM,EAAE2D,KAAK,IAAIV,MAAMnC,GAAG,CAAC8C,KAAK;YAChEC,cAAc;gBAAC;aAAW;WACtBZ,MAAMa,MAAM,GAAG;YAAEA,QAAQb,MAAMa,MAAM;QAAC,IAAI,CAAC;QAEjD,IAAIV,QAAQW,QAAQ,MAAMX,OAAO+E;IACnC,EAAE,OAAO3D,OAAO;QACdC,QAAQC,IAAI,CAAC,uEAAuE;YAAEF,OAAOG,OAAOH;QAAO;IAC7G;IACA,MAAMtB,WAAWlE,sBAAsBmJ,OAAO3E;IAC9C,OAAO2E,MAAM1G,GAAG,CAAC,CAAC2G,OAAOtG;YAAUiC;YAAAA,cAAqBb;gBAArBa,iBAAAA,eAAAA,KAAK,CAACjC,MAAM,qBAAZiC,aAAcQ,GAAG,qBAAIrB,kBAAAA,QAAQ,CAACpB,MAAM,qBAAfoB,gBAAiBqB,GAAG,YAAzCR,OAA6C;;AAClF;AAEA,kGAAkG,GAClG,OAAO,SAASsE,wBAAwBpF,QAA6BxD,gBAAgB;IACnF,OAAO,CAAC,gBAAgB,EAAEwD,MAAMvD,GAAG,CAAC,KAAK,EAAEuD,MAAMtD,GAAG,CAAC,kCAAkC,CAAC;AAC1F;AAEA;;;;;;CAMC,GACD,OAAO,SAAS2I,2BAA2BrD,IAA8B;QACrDA;IAAlB,MAAMsD,aAAYtD,kBAAAA,KAAKsD,SAAS,YAAdtD,kBAAkB/G;IACpC,OAAO,OAAOqH;kBAeUN;YAFNuD;QAZhB,MAAM,EAAE1H,GAAG,EAAE,GAAGyE;QAChB,MAAMkD,SAASF,UAAU;QACzB,MAAM/C,gBAAQD,QAAQE,QAAQ,oBAAhBF,QAAQE,QAAQ,MAAhBF,SAAmB,kCAAmB3G,eAAe;QACnE,IAAI,CAAC6J,UAAU,CAAC3H,IAAIoB,MAAM,EAAE,OAAO,aAAK3C,iBAAiBiG;YAAQE,SAAS;;QAC1E,MAAMgD,UAAU,MAAMzD,KAAKuD,OAAO,CAAC,aAC9BjD;YACHzE,KAAK,aAAKA;gBAAKZ,MAAM;gBAAYF,QAAQ,aAAKc,IAAId,MAAM;oBAAE2I,QAAQ;;;;QAEpE,IAAID,QAAQ7B,OAAO,IAAI6B,QAAQhD,OAAO,IAAIgD,QAAQpC,MAAM,EAAE,OAAOoC;QACjE,MAAMF,UAAUE,QAAQZ,OAAO,CAACrG,GAAG,CAAC,CAAC8F,SAAY,CAAA;gBAAEA;gBAAQK,UAAUtJ,qBAAqBiJ;YAAQ,CAAA,GAAIqB,IAAI,CACxG,CAACC;gBAAUA;mBAAAA,EAAAA,kBAAAA,MAAMjB,QAAQ,qBAAdiB,gBAAgB3I,IAAI,MAAK;;QAEtC,IAAI,CAACsI,WAAWA,EAAAA,oBAAAA,QAAQZ,QAAQ,qBAAhBY,kBAAkBtI,IAAI,MAAK,WAAW,OAAO,aAAKwI;YAASZ,SAAS,EAAE;;QACtF,MAAMgB,WAAWN,QAAQZ,QAAQ,CAACmB,QAAQ,CAACC,KAAK,CAAC,GAAGvJ,iBAAiBE,GAAG;QACxE,MAAMsJ,SAAS,MAAM,EAAChE,eAAAA,KAAKgE,MAAM,YAAXhE,eAAegD,qBAAqB;YACxDnH;YACAoH,OAAOY,SAASrH,GAAG,CAAC,CAACyH,UAAYA,QAAQxG,IAAI;WACzC6C,QAAQzB,MAAM,GAAG;YAAEA,QAAQyB,QAAQzB,MAAM;QAAC,IAAI,CAAC,IAClDgD,KAAK,CAAC,IAAMgC,SAASrH,GAAG,CAAC,IAAM;QAClC,MAAMqG,UAAyB,EAAE;QACjC,IAAIqB,UAAyB;QAC7B,KAAK,MAAM,CAACrH,OAAOoH,QAAQ,IAAIJ,SAASM,OAAO,GAAI;gBAMzC7D,cA6BSiD;YAlCjB,MAAMa,QAAQJ,MAAM,CAACnH,MAAM;YAC3B,MAAMd,UAAU,MAAMyH,OAAOa,MAAM,CAACC,KAAK,CAAC;gBACxCC,OAAO1I,IAAI0I,KAAK;gBAChBtH,QAAQpB,IAAIoB,MAAM;gBAClBoB,KAAKxC,IAAIyC,SAAS;gBAClBkG,GAAG,GAAGlE,eAAAA,QAAQkE,GAAG,YAAXlE,eAAe;gBACrBa,KAAKb,QAAQa,GAAG;gBAChBjG,IAAI4B,gBAAgBjB,IAAI2C,GAAG,EAAE3B;gBAC7BY,MAAMwG,QAAQxG,IAAI;gBAClB,iEAAiE;gBACjE,gEAAgE;gBAChE,gEAAgE;gBAChEsE,SAAS;oBACPtE,MAAMwG,QAAQxG,IAAI;oBAClBgH,MAAMR,QAAQQ,IAAI;oBAClB,mEAAmE;oBACnE,2DAA2D;oBAC3DC,aAAaC,oCAAoCV,QAAQS,WAAW;oBACpEE,MAAMX,QAAQW,IAAI;oBAClBC,SAASZ,QAAQY,OAAO;oBACxBC,UAAUb,QAAQa,QAAQ;oBAC1B5C,gBAAgB+B,QAAQ/B,cAAc;oBACtC6C,YAAY;mBACRX,QAAQ;oBAAEY,WAAW;wBAACZ;qBAAM;gBAAC,IAAI,CAAC;YAE1C;YACA,IAAIrI,QAAQqF,EAAE,KAAK,OAAO;gBACxB8C,UAAUnI,QAAQwD,KAAK;gBACvB;YACF;YACAsD,QAAQoC,IAAI,CAAC;gBACX1C,UAAU;gBACVrH,IAAIa,QAAQb,EAAE;gBACd+B,QAAQpB,IAAIoB,MAAM;gBAClBuF,aAAa,GAAEe,gCAAAA,QAAQjB,MAAM,CAACE,aAAa,YAA5Be,gCAAgC;gBAC/Cd,OAAO1G,QAAQ0B,IAAI;gBACnBiF,MAAMwC;YACR;QACF;QACA,IAAI,CAACrC,QAAQjD,MAAM,EAAE;YACnB,OAAO,aAAK6D;gBAASZ,SAAS,EAAE;gBAAExB,QAAQ3D,YAAYwG,kBAAAA,UAAW;;QACnE;QACA,2EAA2E;QAC3E,IAAIA,SAAS;YACX,MAAMiB,OAAOtC,OAAO,CAACA,QAAQjD,MAAM,GAAG,EAAE;YACxCuF,KAAKzC,IAAI,GAAG,GAAGyC,KAAKzC,IAAI,CAAC,CAAC,EAAEG,QAAQjD,MAAM,CAAC,IAAI,EAAEiE,SAASjE,MAAM,CAAC,QAAQ,EAAEsE,QAAQ,CAAC,CAAC;QACvF;QACA,OAAO,aAAKT;YAASZ;;IACvB;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAeuC,qBACpBC,IAA0B,EAC1B/E,OAAgI,EAChIN,OAAkG,CAAC,CAAC;QAQjFA,oBAORM,cAKKN,iBAMRM;IAxBR,MAAM,EAAEzE,GAAG,EAAEmB,SAAS,EAAE,GAAGsD;IAC3B,IAAI,CAACzE,IAAIoB,MAAM,EAAE,OAAO;IACxB,IAAIoI,SAAS,SAAS;YAEKnI;QADzB,MAAMA,OAAO,MAAMF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACvB,IAAIoB,MAAM,EAAEI,GAAG;QACpE,OAAOrE,iBAAiB,EAACkE,YAAAA,KAAKG,GAAG,CAAC,0BAATH,YAA2B,CAAC,EAAE,CAACrB,IAAIyC,SAAS,CAAC,IAAI,OAAO;IACnF;IACA,MAAMgH,YAAY,EAACtF,qBAAAA,KAAKuF,YAAY,YAAjBvF,qBAAqB7F,mBAAmB;IAC3D,MAAMyH,UAAwC0D,YAC1C,MAAMA,UAAU;QACdtI;QACAuH,OAAO1I,IAAI0I,KAAK;QAChBtH,QAAQpB,IAAIoB,MAAM;QAClBlC,QAAQ;YAAE2I,QAAQ;QAAU;QAC5Bc,GAAG,GAAElE,eAAAA,QAAQkE,GAAG,YAAXlE,eAAe;QACpBjC,KAAKxC,IAAIyC,SAAS;IACpB,KACA;QAAEqD,QAAQ;QAAKpC,OAAO;IAA2C;IACrE,IAAIqC,SAAS,OAAOA,QAAQrC,KAAK;IACjC,MAAMiE,SAAS,EAACxD,kBAAAA,KAAKsD,SAAS,YAAdtD,kBAAkB/G,2BAA2B;IAC7D,IAAI,CAACuK,QAAQ,OAAO;IACpB,MAAMgC,OAAO,MAAMhC,OAAOa,MAAM,CAACoB,OAAO,CAAC;QACvClB,OAAO1I,IAAI0I,KAAK;QAChBtH,QAAQpB,IAAIoB,MAAM;QAClBoB,KAAKxC,IAAIyC,SAAS;QAClBkG,GAAG,GAAGlE,gBAAAA,QAAQkE,GAAG,YAAXlE,gBAAe;QACrBa,KAAKb,QAAQa,GAAG;IAClB;IACA,eAAOqE,wBAAAA,KAAMjG,KAAK,mBAAI;AACxB;AAUA;;;;CAIC,GACD,OAAO,eAAemG,mBACpB1I,SAAsC,EACtCgB,KAAiH,EACjH2H,UAAwCzM,qBAAqB;QAGpC0M,kBAAAA,SACEA,mBAAAA;IAF3B,MAAMA,QAAQ5H,MAAM6E,OAAO,CAAC3G,MAAM,CAAC,CAACoG,SAAWA,OAAOC,QAAQ,KAAK;IACnE,MAAMT,eAAerG,KAAImK,UAAAA,KAAK,CAAC,EAAE,sBAARA,mBAAAA,QAAUjD,QAAQ,qBAAlBiD,gBAAoB,CAAC,eAAe;IAC7D,MAAMhD,iBAAiBnH,KAAImK,WAAAA,KAAK,CAAC,EAAE,sBAARA,oBAAAA,SAAUjD,QAAQ,qBAAlBiD,iBAAoB,CAAC,iBAAiB;IACjE,IAAI,CAAC5H,MAAMnC,GAAG,CAACoB,MAAM,IAAI,CAAC2I,MAAMhG,MAAM,IAAI,CAACkC,cAAc,OAAO;IAChE,IAAI;QACF,MAAM+D,SAAS,MAAMF,QAAQ3I,WAAW;YACtCC,QAAQe,MAAMnC,GAAG,CAACoB,MAAM;YACxB6E;YACAzD,KAAKL,MAAMnC,GAAG,CAACyC,SAAS;YACxBwH,KAAKF,MAAMpJ,GAAG,CAAC,CAACiF,OAASA,KAAKvG,EAAE;YAChCiG,KAAKnD,MAAMmD,GAAG;QAChB;QACA,IAAI,QAAQ0E,QAAQ;YAClBrG,QAAQC,IAAI,CAAC,+BAA+B;gBAAEsG,OAAO/H,MAAMnC,GAAG,CAAC2C,GAAG;gBAAEe,OAAOsG,OAAOtG,KAAK;YAAC;YACxF,OAAO;gBAAEyG,WAAW;gBAAGC,MAAML,MAAMhG,MAAM;gBAAEsG,OAAO,EAAE;YAAC;QACvD;QACA,MAAMjF,QAAQ2E,MACX1J,MAAM,CAAC,CAACuF,OAASoE,OAAOG,SAAS,CAACG,QAAQ,CAAC1E,KAAKvG,EAAE,GAClDsB,GAAG,CAAC,CAACiF;gBAAaA;mBAAJhG,KAAIgG,iBAAAA,KAAKkB,QAAQ,qBAAblB,cAAe,CAAC,OAAO;WACzCvF,MAAM,CAACO;QACV,OAAO;YACLuJ,WAAWH,OAAOG,SAAS,CAACpG,MAAM;YAClCqG,MAAMJ,OAAOI,IAAI,CAACrG,MAAM;YACxBsG,OAAOtD,iBAAiB;gBAAC,CAAC,CAAC,EAAEA,gBAAgB;mBAAK3B,MAAMzE,GAAG,CAAC,CAACqE,OAAS,CAAC,CAAC,EAAE+B,eAAe,CAAC,EAAE/B,MAAM;aAAE,GAAG,EAAE;QAC3G;IACF,EAAE,OAAOtB,OAAO;QACdC,QAAQD,KAAK,CAAC,+BAA+B;YAAEgF,OAAOvG,MAAMnC,GAAG,CAAC0I,KAAK;YAAEwB,OAAO/H,MAAMnC,GAAG,CAAC2C,GAAG;YAAEe;QAAM;QACnG,OAAO;IACT;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAAS6G,eAAepI,KAK9B;QAakB4H,kBAAAA;IAZjB,MAAMS,WAA8B,EAAE;IACtC,MAAMvC,WAAW9F,MAAM6E,OAAO,CAAC3G,MAAM,CAAC,CAACoG,SAAWA,OAAOC,QAAQ,KAAK,aAAa,CAACD,OAAOK,QAAQ;IACnG,IAAImB,SAASlE,MAAM,IAAI5B,MAAMsI,KAAK,EAAE;QAClCD,SAASpB,IAAI,CAAC;YACZ/J,IAAIjB,kBAAkB;YACtBgB,MAAM;YACNwC,MAAM;YACN8I,SAASzC,SAAStH,GAAG,CAAC,CAACyH,UAAYA,QAAQxB,KAAK;YAChD+D,YAAY1C,SAASlE,MAAM,GAAG1F,0BAA0B,YAAY8D,MAAMyI,OAAO,IAAI,EAAE;QACzF;IACF;IACA,MAAMb,QAAQ5H,MAAM6E,OAAO,CAAC3G,MAAM,CAAC,CAACoG,SAAWA,OAAOC,QAAQ,KAAK;IACnE,MAAM1B,OAAOpF,KAAImK,UAAAA,KAAK,CAAC,EAAE,sBAARA,mBAAAA,QAAUjD,QAAQ,qBAAlBiD,gBAAoB,CAAC,iBAAiB;IACvD,IAAIA,MAAMhG,MAAM,IAAIiB,MAAM;QACxBwF,SAASpB,IAAI,CAAC;YACZ/J,IAAIjB,kBAAkB;YACtBgB,MAAM;YACNwC,MAAM;YACN8I,SAASX,MAAMpJ,GAAG,CAAC,CAACiF,OAASA,KAAKgB,KAAK;YACvCiE,MAAM,CAAC,CAAC,EAAE7F,MAAM;YAChB+B,gBAAgB/B;YAChB2F,YAAYtM,0BAA0B,SAAS8D,MAAMyI,OAAO;QAC9D;IACF;IACA,OAAOJ;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAAS1B,oCAAoCD,WAAmB;IACrE,OAAOA,YACJiC,KAAK,CAAC,MACNnK,GAAG,CAAC,CAACoK,OACJA,KACGD,KAAK,CAAC,iBACNzK,MAAM,CAAC,CAAC2K,WAAa,CAAC,aAAaC,IAAI,CAACD,WACxCE,IAAI,CAAC,MAETA,IAAI,CAAC,MACLC,OAAO,CAAC,WAAW,QACnBrL,IAAI;AACT;AAEA,uEAAuE,GACvE,OAAO,MAAMuJ,uBACX,4FAA2F;AAE7F,6GAA6G,GAC7G,OAAO,MAAM+B,8BACX,0FAAyF;AAE3F,0FAA0F,GAC1F,OAAO,SAASC,wBAAwBrE,OAA+B;IACrE,MAAM+C,QAAQ/C,QAAQ3G,MAAM,CAAC,CAACoG,SAAWA,OAAOC,QAAQ,KAAK;IAC7D,MAAMuB,WAAWjB,QAAQ3G,MAAM,CAAC,CAACoG,SAAWA,OAAOC,QAAQ,KAAK,aAAa,CAACD,OAAOK,QAAQ;IAC7F,MAAMwE,QAAkB,EAAE;IAC1B,IAAIvB,MAAMhG,MAAM,EAAE;YACCgG;QAAjB,MAAM/E,OAAOpF,KAAImK,mBAAAA,KAAK,CAAC,EAAE,CAACjD,QAAQ,qBAAjBiD,gBAAmB,CAAC,iBAAiB;QACtDuB,MAAMlC,IAAI,CACR,CAAC,gBAAgB,EAAEpE,OAAO,CAAC,KAAK,EAAEA,MAAM,GAAG,GAAG,kBAAkB,EAAE+E,MAAMpJ,GAAG,CAAC,CAACiF,OAAS,CAAC,CAAC,EAAEA,KAAKgB,KAAK,CAAC,CAAC,CAAC,EAAEsE,IAAI,CAAC,MAAM,CAAC,CAAC,EACtH;IAEJ;IACA,IAAIjD,SAASlE,MAAM,EAAE;QACnBuH,MAAMlC,IAAI,CACR,CAAC,2BAA2B,EAAEnB,SAAStH,GAAG,CAAC,CAACyH,UAAY,CAAC,CAAC,EAAEA,QAAQxB,KAAK,CAAC,CAAC,CAAC,EAAEsE,IAAI,CAAC,MAAM,CAAC,CAAC,EAC3F;IAEJ;IACA,OAAOI;AACT"}
@@ -226,6 +226,14 @@ export declare function aiSiteUnitJob(job: AiJob, unit: AiSiteUnit, built: Built
226
226
  * layout is built, so the header knows the blog before it exists.
227
227
  */
228
228
  export declare function aiSiteWritesPosts(job: Pick<AiJob, 'items'>): boolean;
229
+ /** What the row of a planned page merged into the blog says (AGL-3676). */
230
+ export declare const AI_SITE_BLOG_MERGED_NOTE = "Your blog takes this page\u2019s place: its posts are listed there, and its links go to it.";
231
+ /**
232
+ * Whether this site start sells (AGL-3676): its ledger owes the store's first
233
+ * products and has not given up on them, so the layout built before them
234
+ * carries the cart.
235
+ */
236
+ export declare function aiSiteSellsProducts(job: Pick<AiJob, 'items'>): boolean;
229
237
  /**
230
238
  * A scaffold is admitted with inputs that read, for a site of the job's own
231
239
  * org, and only where this deployment has loaded the step that builds a page: