@aglyn/plugins-ai 1.0.0-beta.263 → 1.0.0-beta.266

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 (109) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/beauty.webp +0 -0
  3. package/src/lib/assets/site-styles/blog.webp +0 -0
  4. package/src/lib/assets/site-styles/business.webp +0 -0
  5. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  6. package/src/lib/assets/site-styles/education.webp +0 -0
  7. package/src/lib/assets/site-styles/events.webp +0 -0
  8. package/src/lib/assets/site-styles/fitness.webp +0 -0
  9. package/src/lib/assets/site-styles/landing.webp +0 -0
  10. package/src/lib/assets/site-styles/music.webp +0 -0
  11. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  12. package/src/lib/assets/site-styles/personal.webp +0 -0
  13. package/src/lib/assets/site-styles/photography.webp +0 -0
  14. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  15. package/src/lib/assets/site-styles/professional.webp +0 -0
  16. package/src/lib/assets/site-styles/realestate.webp +0 -0
  17. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  18. package/src/lib/assets/site-styles/store.webp +0 -0
  19. package/src/lib/assets/site-styles/studio.webp +0 -0
  20. package/src/lib/assets/site-styles/trades.webp +0 -0
  21. package/src/lib/assets/site-styles/wellness.webp +0 -0
  22. package/src/lib/assets/site-styles/yoga.webp +0 -0
  23. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  24. package/src/lib/components/ai-site-start-card.component.js +256 -236
  25. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  26. package/src/lib/components/ai-site-start-steps.component.d.ts +108 -0
  27. package/src/lib/components/ai-site-start-steps.component.js +451 -0
  28. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  29. package/src/lib/components/ai-site-start-tracking.d.ts +114 -0
  30. package/src/lib/components/ai-site-start-tracking.js +277 -0
  31. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  32. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  33. package/src/lib/components/ai-site-style-previews.js +115 -0
  34. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  35. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  36. package/src/lib/insights/ai-insight-digest.js +35 -11
  37. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  39. package/src/lib/jobs/ai-job-page-language.js +20 -2
  40. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  41. package/src/lib/jobs/ai-job-page-sections.js +8 -2
  42. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  43. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  44. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  45. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-site-datasets.d.ts +93 -2
  47. package/src/lib/jobs/ai-job-site-datasets.js +308 -21
  48. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +191 -13
  50. package/src/lib/jobs/ai-layout-stock-photos.js +1709 -69
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  53. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  54. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  59. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  60. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.d.ts +23 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js +163 -0
  63. package/src/lib/jobs/fixtures/ai-stock-photo-juniper-records-recordings.js.map +1 -0
  64. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.d.ts +23 -0
  65. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js +53 -0
  66. package/src/lib/jobs/fixtures/ai-stock-photo-learning-center-recordings.js.map +1 -0
  67. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.d.ts +21 -0
  68. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js +119 -0
  69. package/src/lib/jobs/fixtures/ai-stock-photo-synchronicity-recordings.js.map +1 -0
  70. package/src/lib/layout-language/ai-layout-compiler.js +54 -15
  71. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  72. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  73. package/src/lib/layout-language/ai-layout-design.js +14 -13
  74. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  75. package/src/lib/layout-language/ai-layout-listings.d.ts +55 -0
  76. package/src/lib/layout-language/ai-layout-listings.js +103 -5
  77. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  78. package/src/lib/layout-language/ai-layout-pictures.d.ts +42 -8
  79. package/src/lib/layout-language/ai-layout-pictures.js +91 -6
  80. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  81. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  82. package/src/lib/model/ai-site-home-arcs.js +741 -0
  83. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  84. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  85. package/src/lib/model/ai-site-inventory.js.map +1 -1
  86. package/src/lib/model/ai-site-job.d.ts +9 -6
  87. package/src/lib/model/ai-site-job.js +8 -5
  88. package/src/lib/model/ai-site-job.js.map +1 -1
  89. package/src/lib/model/ai-site-kinds.js +24 -24
  90. package/src/lib/model/ai-site-kinds.js.map +1 -1
  91. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  92. package/src/lib/model/ai-site-start-steps.js +127 -0
  93. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  94. package/src/lib/model/ai-site-start.js +4 -1
  95. package/src/lib/model/ai-site-start.js.map +1 -1
  96. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  97. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  98. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  99. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  100. package/src/lib/runtime/ai-doctrine.js +9 -5
  101. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  102. package/src/lib/runtime/ai-palette.generated.js +5 -2
  103. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  104. package/src/lib/runtime/site-inventory.js +6 -0
  105. package/src/lib/runtime/site-inventory.js.map +1 -1
  106. package/src/lib/server/ai-insight-digest-route.js +2 -2
  107. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  108. package/src/lib/usage/assist-free-taste.js +3 -0
  109. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -15,7 +15,8 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
- import { type AiBuildPlanCreate, type AiBuildPlanScreen } from '../model/ai-build-plan';
18
+ import { type AiBuildPlan, type AiBuildPlanCreate, type AiBuildPlanScreen } from '../model/ai-build-plan';
19
+ import type { AiInventoryDataset, AiSiteInventory } from '../model/ai-site-inventory';
19
20
  import type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types';
20
21
  import type { AiPlanCapabilities } from '../model/ai-plan-capabilities';
21
22
  import { generateAiDataset } from '../runtime/ai-dataset-generation';
@@ -143,12 +144,20 @@ export declare function aiSiteFormDatasetNote(form: string): string;
143
144
  export declare const AI_SITE_DATASET_PHOTOS_BUDGET_MS = 20000;
144
145
  /** The field a pictured dataset's photo is kept in, where the plan gave it none. */
145
146
  export declare const AI_SITE_DATASET_IMAGE_FIELD = "Image";
147
+ /**
148
+ * The field a pictured dataset keeps what each record's photo shows in
149
+ * (AGL-3856), which its card's and its page's Image take as their alt text:
150
+ * a record's title is what it is called, never what its photo shows.
151
+ */
152
+ export declare const AI_SITE_DATASET_IMAGE_ALT_FIELD = "Image description";
146
153
  /** One record's photo: the address its Image shows, and the library asset placed, which no page shows again. */
147
154
  export interface AiSiteDatasetRecordPhoto {
148
155
  /** The media library's CDN path, or a starter's site path; `null` for none. */
149
156
  src: string | null;
150
157
  /** The `media:` reference of the stock photo placed, or `null` for a starter. */
151
158
  placed: string | null;
159
+ /** What the photo shows, in its own words (AGL-3856); `null` for none. */
160
+ alt?: string | null;
152
161
  }
153
162
  /**
154
163
  * A photo for each record of a pictured dataset (AGL-3616), in order: a stock
@@ -167,6 +176,8 @@ export declare function aiSiteDatasetRecordPhotos(input: {
167
176
  name: string;
168
177
  /** Each record's name, by position. */
169
178
  names: readonly string[];
179
+ /** What each record is filed under, by position ({@link aiSiteDatasetCategoryOf}, AGL-3856); absent for a list that files none. */
180
+ categories?: ReadonlyArray<string | null>;
170
181
  pictures: AiSiteDatasetPictures;
171
182
  signal?: AbortSignal;
172
183
  stockPhotos?: typeof aiLayoutStockPhotoSource;
@@ -178,13 +189,32 @@ export declare function aiSiteDatasetRecordPhotos(input: {
178
189
  * and each record holds its photo there; a record with none holds nothing.
179
190
  * Pure.
180
191
  */
181
- export declare function aiSiteDatasetContentWithPhotos(content: Record<string, unknown>, photos: ReadonlyArray<string | null>): Record<string, unknown>;
192
+ export declare function aiSiteDatasetContentWithPhotos(content: Record<string, unknown>, photos: ReadonlyArray<string | null>,
193
+ /** What each record's photo shows (AGL-3856), kept in {@link AI_SITE_DATASET_IMAGE_ALT_FIELD}; none adds no field. */
194
+ alts?: ReadonlyArray<string | null>): Record<string, unknown>;
182
195
  export interface AiSiteDatasetRunnerDeps {
183
196
  generate?: typeof generateAiDataset;
184
197
  writerFor?: typeof pluginResourceDraftWriter;
185
198
  /** Where a pictured dataset's photos come from; the stock library, else the starters, otherwise. */
186
199
  photos?: typeof aiSiteDatasetRecordPhotos;
187
200
  }
201
+ /**
202
+ * What a designed dataset's records are filed under (AGL-3921): its first
203
+ * field whose name says it files them ("Category", "Type", "Session type"),
204
+ * with each record's value, where they hold at least two different values
205
+ * and some record shares one — a list that files every record apart files
206
+ * none. `null` otherwise. Pure.
207
+ */
208
+ export declare function aiSiteDatasetCategoryOf(dataset: {
209
+ fields: ReadonlyArray<{
210
+ name: string;
211
+ }>;
212
+ records: ReadonlyArray<ReadonlyArray<string | undefined>>;
213
+ }): {
214
+ column: number;
215
+ name: string;
216
+ values: Array<string | null>;
217
+ } | null;
188
218
  /**
189
219
  * The fields of a designed dataset that its records leave empty on most of
190
220
  * them — every record, or more than half — by the id the data plugin gave
@@ -239,6 +269,11 @@ export declare function aiDatasetListingsOf(datasets: ReadonlyArray<{
239
269
  imageField?: string | null;
240
270
  /** The record's page-address field, where the writer added one (AGL-3727). */
241
271
  addressField?: string | null;
272
+ /** What its records are filed under, by position as `records` (AGL-3921): a section named for one value lists its records alone. */
273
+ groups?: {
274
+ field: string;
275
+ values: readonly string[];
276
+ } | null;
242
277
  }>, screens: readonly AiLayoutListingScreen[]): AiLayoutListing[];
243
278
  /**
244
279
  * A `records` listing for each dataset the workspace already keeps that a
@@ -257,6 +292,62 @@ export declare function aiSiteReusedDatasetListings(input: {
257
292
  /** The listings the start already has, whose datasets are not listed twice. */
258
293
  listed: ReadonlyArray<Pick<AiLayoutListing, 'datasetId'>>;
259
294
  }): AiLayoutListing[];
295
+ /**
296
+ * A SITE START NEVER SHOWS ANOTHER SITE'S RECORDS (AGL-3912).
297
+ *
298
+ * Datasets belong to the organization, and so do their records: neither
299
+ * names the site it was written for, and a dataset outlives the site that
300
+ * filled it (the data plugin keeps `visibleTo` and nothing else of a site). A
301
+ * gym's start reused the workspace's "Session packages", "Coaches" and "Gym
302
+ * FAQs", and published a deleted photographer's wedding packages and a
303
+ * deleted studio's coach bio ("Leads coaching at Ironline Strength Studio")
304
+ * with four broken photos; a photographer's start reused another
305
+ * photographer's gallery and drew six broken covers.
306
+ *
307
+ * So a site start reuses a dataset only when its records are this site's to
308
+ * show ({@link aiSiteDatasetIsThisSites}):
309
+ * - it holds no records at all — an empty list, such as the one a form
310
+ * writes its submissions to, is the same list on any site; or
311
+ * - its scope names this site alone (`siteOnly`), so it was made for it; or
312
+ * - this site already serves its records pages of their own (`recordBase`).
313
+ * Anything else — records in a dataset shared with the workspace or with
314
+ * other sites, and a dataset whose records could not be counted — was filled
315
+ * for something else, and the plan is settled in code before the rules read
316
+ * it ({@link aiSettleSiteForeignDatasets}): the reuse becomes a creation of
317
+ * the same name and fields, designed from this site's brief with photos of
318
+ * its own, and every section, record template and form that named it names
319
+ * the creation. Where this job may create no dataset, the reuse comes off
320
+ * instead and each section writes its items out. Never a refusal, and
321
+ * nothing a member is asked.
322
+ *
323
+ * An Assist change to a site that exists keeps reusing what the site lists:
324
+ * this is a site start's rule, and only its plan is settled by it.
325
+ */
326
+ export declare function aiSiteDatasetIsThisSites(row: Pick<AiInventoryDataset, 'records' | 'siteOnly' | 'recordBase'>): boolean;
327
+ /**
328
+ * A site start's plan with every dataset it reuses that holds another
329
+ * site's records made again for this one (AGL-3912): each the inventory marks
330
+ * `foreign` ({@link aiSiteInventoryWithRecordCounts}, by
331
+ * {@link aiSiteDatasetIsThisSites}). The creation keeps the dataset's name,
332
+ * which rule 7 does not hold against it as a duplicate. Pure; a plan that
333
+ * reuses none comes back as it was.
334
+ */
335
+ export declare function aiSettleSiteForeignDatasets(plan: AiBuildPlan, inventory: Pick<AiSiteInventory, 'datasets'> | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
336
+ /** How a dataset's records are counted: the data plugin's index, which answers `facts.records`. */
337
+ export type AiDatasetRecordCounter = (request: {
338
+ orgId: string;
339
+ id: string;
340
+ }) => Promise<number | null>;
341
+ /** The production counter: the data plugin's `dataset` index, read one dataset at a time. */
342
+ export declare const aiDatasetRecordCount: AiDatasetRecordCounter;
343
+ /**
344
+ * A site start's inventory with how many records each dataset holds
345
+ * (AGL-3912), and each whose records are not this site's to show
346
+ * ({@link aiSiteDatasetIsThisSites}) marked `foreign`. The count is asked
347
+ * once per plan, of every dataset the site sees, all at once; a count that
348
+ * fails leaves `records` out, which reads as another site's. Never throws.
349
+ */
350
+ export declare function aiSiteInventoryWithRecordCounts(inventory: AiSiteInventory | null, orgId: string, count?: AiDatasetRecordCounter): Promise<AiSiteInventory | null>;
260
351
  /**
261
352
  * The site's datasets a layout or a page unit is handed (AGL-3616), each a
262
353
  * `records` listing with the sections of the plan's pages that name it, once
@@ -16,7 +16,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { checkDatasetQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
18
  import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
19
- import { isAiPlanNewRef } from "../model/ai-build-plan.js";
19
+ import { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
20
+ import { AI_BUILD_PLAN_LIMITS, AI_PLAN_NEW_REF_PREFIX, isAiPlanNewRef } from "../model/ai-build-plan.js";
21
+ import { AI_TYPED_LIST_MIN_ITEMS } from "../runtime/ai-doctrine-validators.js";
20
22
  import { AI_SITE_DATASETS_MAX } from "../model/ai-site-job.js";
21
23
  import { AI_STEP_TIERS } from "../providers/catalog.js";
22
24
  import { aiModelForStep } from "../providers/routing.js";
@@ -219,6 +221,11 @@ const limitReview = (message)=>({
219
221
  });
220
222
  /** The wall clock a dataset's photos may take inside its pass: the 20 seconds a page's pictures had (AGL-3660). */ export const AI_SITE_DATASET_PHOTOS_BUDGET_MS = 20000;
221
223
  /** The field a pictured dataset's photo is kept in, where the plan gave it none. */ export const AI_SITE_DATASET_IMAGE_FIELD = 'Image';
224
+ /**
225
+ * The field a pictured dataset keeps what each record's photo shows in
226
+ * (AGL-3856), which its card's and its page's Image take as their alt text:
227
+ * a record's title is what it is called, never what its photo shows.
228
+ */ export const AI_SITE_DATASET_IMAGE_ALT_FIELD = 'Image description';
222
229
  /** A planned field that already names a record's photo. */ const IMAGE_FIELD_NAME = /^(?:main |cover |hero |featured? )?(?:image|photo|picture|photograph|thumbnail|headshot|portrait|cover)s?$/i;
223
230
  /**
224
231
  * A photo for each record of a pictured dataset (AGL-3616), in order: a stock
@@ -237,7 +244,9 @@ const limitReview = (message)=>({
237
244
  placed: null
238
245
  }));
239
246
  const people = input.pictures === 'people';
240
- const slots = input.names.map((name, index)=>({
247
+ const slots = input.names.map((name, index)=>{
248
+ var _input_categories_index, _input_categories;
249
+ return _extends({
241
250
  imageId: `record${index}`,
242
251
  frameId: null,
243
252
  iconId: null,
@@ -245,8 +254,17 @@ const limitReview = (message)=>({
245
254
  // The record card's own shape: a team's portraits stand, pieces sit wide.
246
255
  aspect: people ? 4 / 5 : 4 / 3,
247
256
  sectionIndex: 0,
248
- role: people ? 'about' : 'gallery'
249
- }));
257
+ role: people ? 'about' : 'gallery',
258
+ // A record's name is a search's words, never what its photo shows (AGL-3856).
259
+ standIn: true
260
+ }, people ? {
261
+ people: true
262
+ } : {}, people ? {} : {
263
+ record: ((_input_categories = input.categories) == null ? void 0 : (_input_categories_index = _input_categories[index]) == null ? void 0 : _input_categories_index.trim()) ? {
264
+ category: input.categories[index].trim()
265
+ } : {}
266
+ });
267
+ });
250
268
  const seed = `${job.$id}:records`;
251
269
  let found = [];
252
270
  try {
@@ -278,15 +296,17 @@ const limitReview = (message)=>({
278
296
  const hostId = job.hostId;
279
297
  return slots.map((_slot, index)=>{
280
298
  var _ref;
281
- var _found_index, _found_index1, _starters_index;
299
+ var _found_index, _found_index1, _found_index_alt, _found_index2, _starters_index, _starters_index_alt, _starters_index1;
282
300
  const stock = aiSiteProductPhotoSrc((_found_index = found[index]) == null ? void 0 : _found_index.src, hostId);
283
301
  if (stock) return {
284
302
  src: stock,
285
- placed: (_ref = (_found_index1 = found[index]) == null ? void 0 : _found_index1.src) != null ? _ref : null
303
+ placed: (_ref = (_found_index1 = found[index]) == null ? void 0 : _found_index1.src) != null ? _ref : null,
304
+ alt: ((_found_index2 = found[index]) == null ? void 0 : (_found_index_alt = _found_index2.alt) == null ? void 0 : _found_index_alt.trim()) || null
286
305
  };
287
306
  return {
288
307
  src: aiSiteProductPhotoSrc((_starters_index = starters[index]) == null ? void 0 : _starters_index.src, hostId),
289
- placed: null
308
+ placed: null,
309
+ alt: ((_starters_index1 = starters[index]) == null ? void 0 : (_starters_index_alt = _starters_index1.alt) == null ? void 0 : _starters_index_alt.trim()) || null
290
310
  };
291
311
  });
292
312
  }
@@ -296,11 +316,11 @@ const limitReview = (message)=>({
296
316
  * plugin's `image` field, else one called "Image" is added after the others,
297
317
  * and each record holds its photo there; a record with none holds nothing.
298
318
  * Pure.
299
- */ export function aiSiteDatasetContentWithPhotos(content, photos) {
319
+ */ export function aiSiteDatasetContentWithPhotos(content, photos, /** What each record's photo shows (AGL-3856), kept in {@link AI_SITE_DATASET_IMAGE_ALT_FIELD}; none adds no field. */ alts = []) {
300
320
  const fields = Array.isArray(content['fields']) ? content['fields'] : [];
301
321
  const named = fields.find((field)=>IMAGE_FIELD_NAME.test(str(field.name)));
302
322
  const fieldName = named ? named.name : AI_SITE_DATASET_IMAGE_FIELD;
303
- const nextFields = named ? fields.map((field)=>field === named ? _extends({}, field, {
323
+ const withImage = named ? fields.map((field)=>field === named ? _extends({}, field, {
304
324
  type: 'image'
305
325
  }) : field) : [
306
326
  ...fields,
@@ -309,6 +329,15 @@ const limitReview = (message)=>({
309
329
  type: 'image'
310
330
  }
311
331
  ];
332
+ const described = alts.some((alt, index)=>Boolean(alt && photos[index]));
333
+ const altName = AI_SITE_DATASET_IMAGE_ALT_FIELD;
334
+ const nextFields = described && !withImage.some((field)=>str(field.name).toLowerCase() === altName.toLowerCase()) ? [
335
+ ...withImage,
336
+ {
337
+ name: altName,
338
+ type: 'text'
339
+ }
340
+ ] : withImage;
312
341
  const records = Array.isArray(content['records']) ? content['records'] : [];
313
342
  return _extends({}, content, {
314
343
  fields: nextFields,
@@ -316,12 +345,60 @@ const limitReview = (message)=>({
316
345
  const next = _extends({}, record);
317
346
  delete next[fieldName];
318
347
  const photo = photos[index];
348
+ const alt = photo && described ? alts[index] : null;
319
349
  return photo ? _extends({}, next, {
320
350
  [fieldName]: photo
321
- }) : next;
351
+ }, alt ? {
352
+ [altName]: alt
353
+ } : {}) : next;
322
354
  })
323
355
  });
324
356
  }
357
+ /** A field whose name says it files its records: a category, a type, a kind. */ const CATEGORY_FIELD = /^(?:(?:gallery|session|service|event|course|class|product|menu|work|project) )?(?:category|categories|type|kind|collection|genre|group|section|subject|occasion|series)$/i;
358
+ /**
359
+ * What a designed dataset's records are filed under (AGL-3921): its first
360
+ * field whose name says it files them ("Category", "Type", "Session type"),
361
+ * with each record's value, where they hold at least two different values
362
+ * and some record shares one — a list that files every record apart files
363
+ * none. `null` otherwise. Pure.
364
+ */ export function aiSiteDatasetCategoryOf(dataset) {
365
+ for (const [column, field] of dataset.fields.entries()){
366
+ if (column === 0 || !CATEGORY_FIELD.test(field.name.trim())) continue;
367
+ const values = dataset.records.map((values)=>{
368
+ var _values_column;
369
+ return ((_values_column = values[column]) != null ? _values_column : '').replace(/\s+/g, ' ').trim() || null;
370
+ });
371
+ const distinct = new Set(values.filter((value)=>Boolean(value)).map((value)=>value.toLowerCase()));
372
+ if (distinct.size >= 2 && distinct.size < values.filter(Boolean).length) return {
373
+ column,
374
+ name: field.name,
375
+ values
376
+ };
377
+ }
378
+ return null;
379
+ }
380
+ /**
381
+ * A dataset's groups as its proposal keeps them: the written field's id, and
382
+ * the value of each record its {@link AiSiteDatasetProposal.recordNames} names.
383
+ */ function aiSiteDatasetGroupsOf(category, written, designed) {
384
+ var _ref;
385
+ var _written_find, _written_category_column;
386
+ if (!category) return {};
387
+ const field = (_ref = (_written_find = written.find((entry)=>entry.name.trim().toLowerCase() === category.name.trim().toLowerCase())) == null ? void 0 : _written_find.id) != null ? _ref : written.length === designed.fields.length ? (_written_category_column = written[category.column]) == null ? void 0 : _written_category_column.id : undefined;
388
+ if (!field) return {};
389
+ const values = designed.records.flatMap((record, index)=>{
390
+ var _category_values_index;
391
+ return record[0] ? [
392
+ (_category_values_index = category.values[index]) != null ? _category_values_index : ''
393
+ ] : [];
394
+ }).slice(0, 12);
395
+ return {
396
+ groups: {
397
+ field,
398
+ values
399
+ }
400
+ };
401
+ }
325
402
  /**
326
403
  * The fields of a designed dataset that its records leave empty on most of
327
404
  * them — every record, or more than half — by the id the data plugin gave
@@ -350,7 +427,7 @@ const limitReview = (message)=>({
350
427
  function proposalOf(output) {
351
428
  var _output_proposal;
352
429
  const raw = (_output_proposal = output.proposal) != null ? _output_proposal : {};
353
- return {
430
+ return _extends({
354
431
  fields: Array.isArray(raw.fields) ? raw.fields.filter((field)=>field && typeof field.id === 'string' && field.id).map((field)=>({
355
432
  id: field.id,
356
433
  name: str(field.name) || field.id,
@@ -361,7 +438,12 @@ function proposalOf(output) {
361
438
  imageField: str(raw.imageField) || null,
362
439
  photos: Array.isArray(raw.photos) ? raw.photos.map(str).filter((src)=>src.startsWith('media:')).slice(0, 24) : [],
363
440
  sparseFields: Array.isArray(raw.sparseFields) ? raw.sparseFields.map(str).filter(Boolean) : []
364
- };
441
+ }, raw.groups && typeof raw.groups === 'object' && str(raw.groups.field) && Array.isArray(raw.groups.values) ? {
442
+ groups: {
443
+ field: str(raw.groups.field),
444
+ values: raw.groups.values.slice(0, 12).map(str)
445
+ }
446
+ } : {});
365
447
  }
366
448
  /**
367
449
  * The dataset unit's runner, on the job the scaffold derived for it: the one
@@ -374,7 +456,7 @@ function proposalOf(output) {
374
456
  const writerFor = (_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter;
375
457
  const photosFor = (_deps_photos = deps.photos) != null ? _deps_photos : aiSiteDatasetRecordPhotos;
376
458
  return async (context)=>{
377
- var _ref, _context_org, _facts_fields, _facts_addressField, _facts_fields1, _facts_records;
459
+ var _ref, _context_org, _facts_fields, _facts_addressField, _facts_fields1, _facts_fields2, _facts_records;
378
460
  var _job_plan_create, _job_plan;
379
461
  const { job, firestore, signal } = context;
380
462
  const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, AI_DATASET_STEP)) != null ? _ref : aiModelForStep(AI_DATASET_STEP);
@@ -408,10 +490,10 @@ function proposalOf(output) {
408
490
  id: job.$id
409
491
  });
410
492
  if (written) {
411
- var _facts_fields2, _facts_addressField1, _facts_imageField;
493
+ var _facts_fields3, _facts_addressField1, _facts_imageField;
412
494
  const facts = written.facts;
413
495
  const again = output(written.id, written.name || creation.name, {
414
- fields: (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [],
496
+ fields: (_facts_fields3 = facts.fields) != null ? _facts_fields3 : [],
415
497
  recordNames: [],
416
498
  addressField: (_facts_addressField1 = facts.addressField) != null ? _facts_addressField1 : null,
417
499
  imageField: (_facts_imageField = facts.imageField) != null ? _facts_imageField : null
@@ -427,7 +509,7 @@ function proposalOf(output) {
427
509
  // A form's dataset (AGL-3616): its records are the submissions, so it is
428
510
  // designed from the plan with no model, and starts empty.
429
511
  if (input.forForm) {
430
- var _context_org1, _facts_fields3;
512
+ var _context_org1, _facts_fields4;
431
513
  const content = aiSiteFormDatasetContent(creation.name, creation.fields, input.forForm);
432
514
  const checked = keeper.writer.check(content, {
433
515
  hostId: job.hostId
@@ -464,7 +546,7 @@ function proposalOf(output) {
464
546
  return aiUnspentOutcome(model, {
465
547
  outputs: [
466
548
  _extends({}, output(result.id, result.name, {
467
- fields: (_facts_fields3 = facts.fields) != null ? _facts_fields3 : [],
549
+ fields: (_facts_fields4 = facts.fields) != null ? _facts_fields4 : [],
468
550
  recordNames: [],
469
551
  addressField: null
470
552
  }, 0), {
@@ -496,6 +578,8 @@ function proposalOf(output) {
496
578
  const designed = aiDatasetDraftContent(creation.name, generation.value, {
497
579
  recordPages: input.recordPages
498
580
  });
581
+ // What each record is filed under, which its photo and the sections that list it by its value read (AGL-3856).
582
+ const category = aiSiteDatasetCategoryOf(generation.value);
499
583
  // A list a visitor looks at keeps its records' photos (AGL-3616): searched
500
584
  // in code, never asked of the model, so they cost no credits.
501
585
  const photos = input.pictures ? await photosFor(_extends({
@@ -504,7 +588,10 @@ function proposalOf(output) {
504
588
  names: generation.value.records.map((values)=>{
505
589
  var _values_;
506
590
  return (_values_ = values[0]) != null ? _values_ : '';
507
- }),
591
+ })
592
+ }, category ? {
593
+ categories: category.values
594
+ } : {}, {
508
595
  pictures: input.pictures
509
596
  }, signal ? {
510
597
  signal
@@ -513,6 +600,10 @@ function proposalOf(output) {
513
600
  var _ref;
514
601
  var _photos_index;
515
602
  return (_ref = (_photos_index = photos[index]) == null ? void 0 : _photos_index.src) != null ? _ref : null;
603
+ }), generation.value.records.map((_values, index)=>{
604
+ var _ref;
605
+ var _photos_index;
606
+ return (_ref = (_photos_index = photos[index]) == null ? void 0 : _photos_index.alt) != null ? _ref : null;
516
607
  })) : designed;
517
608
  // The data plugin's own rules, before anything is written: content we
518
609
  // made that its model refuses is our failure, refunded.
@@ -565,7 +656,7 @@ function proposalOf(output) {
565
656
  photos: placed
566
657
  } : {}, {
567
658
  sparseFields: aiDatasetSparseFields(generation.value, (_facts_fields1 = facts.fields) != null ? _facts_fields1 : [])
568
- });
659
+ }, aiSiteDatasetGroupsOf(category, (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [], generation.value));
569
660
  return _extends({}, spent, {
570
661
  outputs: [
571
662
  output(result.id, result.name, proposal, (_facts_records = facts.records) != null ? _facts_records : generation.value.records.length)
@@ -702,6 +793,201 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
702
793
  });
703
794
  return aiDatasetListingsOf(datasets, input.screens);
704
795
  }
796
+ /**
797
+ * A SITE START NEVER SHOWS ANOTHER SITE'S RECORDS (AGL-3912).
798
+ *
799
+ * Datasets belong to the organization, and so do their records: neither
800
+ * names the site it was written for, and a dataset outlives the site that
801
+ * filled it (the data plugin keeps `visibleTo` and nothing else of a site). A
802
+ * gym's start reused the workspace's "Session packages", "Coaches" and "Gym
803
+ * FAQs", and published a deleted photographer's wedding packages and a
804
+ * deleted studio's coach bio ("Leads coaching at Ironline Strength Studio")
805
+ * with four broken photos; a photographer's start reused another
806
+ * photographer's gallery and drew six broken covers.
807
+ *
808
+ * So a site start reuses a dataset only when its records are this site's to
809
+ * show ({@link aiSiteDatasetIsThisSites}):
810
+ * - it holds no records at all — an empty list, such as the one a form
811
+ * writes its submissions to, is the same list on any site; or
812
+ * - its scope names this site alone (`siteOnly`), so it was made for it; or
813
+ * - this site already serves its records pages of their own (`recordBase`).
814
+ * Anything else — records in a dataset shared with the workspace or with
815
+ * other sites, and a dataset whose records could not be counted — was filled
816
+ * for something else, and the plan is settled in code before the rules read
817
+ * it ({@link aiSettleSiteForeignDatasets}): the reuse becomes a creation of
818
+ * the same name and fields, designed from this site's brief with photos of
819
+ * its own, and every section, record template and form that named it names
820
+ * the creation. Where this job may create no dataset, the reuse comes off
821
+ * instead and each section writes its items out. Never a refusal, and
822
+ * nothing a member is asked.
823
+ *
824
+ * An Assist change to a site that exists keeps reusing what the site lists:
825
+ * this is a site start's rule, and only its plan is settled by it.
826
+ */ export function aiSiteDatasetIsThisSites(row) {
827
+ return row.records === 0 || row.siteOnly === true || Boolean(row.recordBase);
828
+ }
829
+ /** A planned field that is a record's page address, which the writer adds itself. */ const ADDRESS_FIELD_NAME = /^(?:page )?(?:address|slug|url|link)$/i;
830
+ /** The fields a dataset is made again with: its own, less the photo, its description and the page address the writer adds. */ function remadeFields(row) {
831
+ var _row_fieldIds;
832
+ const ids = (_row_fieldIds = row.fieldIds) != null ? _row_fieldIds : [];
833
+ return row.fields.filter((name, index)=>{
834
+ const id = ids[index];
835
+ if (id && (id === row.imageField || id === row.addressField)) return false;
836
+ if (IMAGE_FIELD_NAME.test(name.trim()) || ADDRESS_FIELD_NAME.test(name.trim())) return false;
837
+ return name.trim().toLowerCase() !== AI_SITE_DATASET_IMAGE_ALT_FIELD.toLowerCase();
838
+ }).slice(0, AI_BUILD_PLAN_LIMITS.fields);
839
+ }
840
+ /**
841
+ * A site start's plan with every dataset it reuses that holds another
842
+ * site's records made again for this one (AGL-3912): each the inventory marks
843
+ * `foreign` ({@link aiSiteInventoryWithRecordCounts}, by
844
+ * {@link aiSiteDatasetIsThisSites}). The creation keeps the dataset's name,
845
+ * which rule 7 does not hold against it as a duplicate. Pure; a plan that
846
+ * reuses none comes back as it was.
847
+ */ export function aiSettleSiteForeignDatasets(plan, inventory, capabilities = null) {
848
+ var _ref, _plan_items;
849
+ const rows = new Map(((_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : []).map((row)=>[
850
+ row.id,
851
+ row
852
+ ]));
853
+ const foreign = plan.reuse.filter((entry)=>{
854
+ var _rows_get;
855
+ return entry.kind === 'dataset' && ((_rows_get = rows.get(entry.id)) == null ? void 0 : _rows_get.foreign) === true;
856
+ });
857
+ if (!foreign.length) return plan;
858
+ const creatable = !capabilities || capabilities.create.dataset.allowed;
859
+ const taken = new Map([
860
+ ...plan.create.map((entry)=>[
861
+ entry.name.trim().toLowerCase(),
862
+ entry
863
+ ]),
864
+ ...((_plan_items = plan.items) != null ? _plan_items : []).map((item)=>[
865
+ item.name.trim().toLowerCase(),
866
+ null
867
+ ])
868
+ ]);
869
+ // Each foreign dataset's id → the `new:` reference that replaces it, or `null` where it comes off.
870
+ const replaced = new Map();
871
+ const added = [];
872
+ for (const entry of foreign){
873
+ const row = rows.get(entry.id);
874
+ const base = row.name.trim() || 'Records';
875
+ if (!creatable) {
876
+ replaced.set(entry.id, null);
877
+ continue;
878
+ }
879
+ // The plan already makes one of that name: that one is this site's.
880
+ const same = taken.get(base.toLowerCase());
881
+ if ((same == null ? void 0 : same.kind) === 'dataset') {
882
+ replaced.set(entry.id, `${AI_PLAN_NEW_REF_PREFIX}${same.name}`);
883
+ continue;
884
+ }
885
+ let name = base;
886
+ for(let n = 2; taken.has(name.toLowerCase()); n += 1)name = `${base} ${n}`;
887
+ const creation = {
888
+ kind: 'dataset',
889
+ name,
890
+ why: entry.purpose || `Lists ${base} on this site.`,
891
+ duplicateOf: null,
892
+ fields: remadeFields(row)
893
+ };
894
+ added.push(creation);
895
+ taken.set(name.toLowerCase(), creation);
896
+ replaced.set(entry.id, `${AI_PLAN_NEW_REF_PREFIX}${name}`);
897
+ }
898
+ const swap = (ref)=>{
899
+ var _replaced_get;
900
+ return replaced.has(ref) ? (_replaced_get = replaced.get(ref)) != null ? _replaced_get : null : ref;
901
+ };
902
+ return _extends({}, plan, {
903
+ reuse: plan.reuse.filter((entry)=>!(entry.kind === 'dataset' && replaced.has(entry.id))),
904
+ create: [
905
+ ...plan.create.map((entry)=>{
906
+ if (entry.kind !== 'form' || !entry.writesTo || !replaced.has(entry.writesTo)) return entry;
907
+ const writesTo = swap(entry.writesTo);
908
+ if (writesTo) return _extends({}, entry, {
909
+ writesTo
910
+ });
911
+ // A form whose dataset comes off sends its submissions to the Inbox only.
912
+ const rest = _extends({}, entry);
913
+ delete rest.writesTo;
914
+ return rest;
915
+ }),
916
+ ...added
917
+ ],
918
+ screens: plan.screens.map((screen)=>{
919
+ // A record template of a dataset made again shows the new one; of one that comes off, it is a page of its own.
920
+ const record = screen.record && replaced.has(screen.record.dataset) ? {
921
+ record: swap(screen.record.dataset)
922
+ } : null;
923
+ const sections = screen.sections.map((section)=>{
924
+ if (!section.uses.some((ref)=>replaced.has(ref))) return section;
925
+ const uses = section.uses.flatMap((ref)=>{
926
+ const next = swap(ref);
927
+ return next ? [
928
+ next
929
+ ] : [];
930
+ });
931
+ // A list that comes off is written out on the page, as fewer items than a dataset binds.
932
+ const dropped = section.uses.some((ref)=>replaced.has(ref) && !replaced.get(ref));
933
+ return _extends({}, section, {
934
+ uses
935
+ }, dropped ? {
936
+ items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
937
+ } : {});
938
+ });
939
+ return _extends({}, screen, record && screen.record ? {
940
+ record: record.record ? _extends({}, screen.record, {
941
+ dataset: record.record
942
+ }) : null
943
+ } : {}, {
944
+ sections
945
+ });
946
+ })
947
+ });
948
+ }
949
+ /** The production counter: the data plugin's `dataset` index, read one dataset at a time. */ export const aiDatasetRecordCount = async ({ orgId, id })=>{
950
+ var _pluginRecordIndex;
951
+ const index = (_pluginRecordIndex = pluginRecordIndex('dataset')) == null ? void 0 : _pluginRecordIndex.index;
952
+ if (!index) return null;
953
+ const found = await index.get({
954
+ orgId,
955
+ id
956
+ });
957
+ const records = found == null ? void 0 : found.facts['records'];
958
+ return typeof records === 'number' && Number.isFinite(records) ? records : null;
959
+ };
960
+ /**
961
+ * A site start's inventory with how many records each dataset holds
962
+ * (AGL-3912), and each whose records are not this site's to show
963
+ * ({@link aiSiteDatasetIsThisSites}) marked `foreign`. The count is asked
964
+ * once per plan, of every dataset the site sees, all at once; a count that
965
+ * fails leaves `records` out, which reads as another site's. Never throws.
966
+ */ export async function aiSiteInventoryWithRecordCounts(inventory, orgId, count = aiDatasetRecordCount) {
967
+ if (!(inventory == null ? void 0 : inventory.datasets.length)) return inventory;
968
+ const counts = await Promise.all(inventory.datasets.map((row)=>count({
969
+ orgId,
970
+ id: row.id
971
+ }).catch((error)=>{
972
+ console.warn('ai site plan: a dataset’s records could not be counted', {
973
+ orgId,
974
+ id: row.id,
975
+ error: String(error)
976
+ });
977
+ return null;
978
+ })));
979
+ return _extends({}, inventory, {
980
+ datasets: inventory.datasets.map((row, index)=>{
981
+ const records = counts[index];
982
+ const counted = typeof records === 'number' ? _extends({}, row, {
983
+ records
984
+ }) : row;
985
+ return aiSiteDatasetIsThisSites(counted) ? counted : _extends({}, counted, {
986
+ foreign: true
987
+ });
988
+ })
989
+ });
990
+ }
705
991
  /**
706
992
  * The site's datasets a layout or a page unit is handed (AGL-3616), each a
707
993
  * `records` listing with the sections of the plan's pages that name it, once
@@ -709,7 +995,7 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
709
995
  * nowhere: the pages built without it write the items out.
710
996
  */ export function aiSiteDatasetListings(input) {
711
997
  const made = input.datasets.flatMap((dataset)=>{
712
- var _proposal_imageField;
998
+ var _proposal_imageField, _proposal_groups;
713
999
  const id = input.delivered.get(dataset.name.trim().toLowerCase());
714
1000
  const output = id ? input.outputs.find((entry)=>entry.resource === 'draft' && entry.draftResource === AI_SITE_DATASET_RESOURCE && entry.id === id) : undefined;
715
1001
  if (!id || !output) return [];
@@ -728,7 +1014,8 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
728
1014
  return field.id !== proposal.addressField && field.id !== proposal.imageField && (index === 0 || !((_proposal_sparseFields = proposal.sparseFields) == null ? void 0 : _proposal_sparseFields.includes(field.id)));
729
1015
  }),
730
1016
  imageField: (_proposal_imageField = proposal.imageField) != null ? _proposal_imageField : null,
731
- addressField: proposal.addressField
1017
+ addressField: proposal.addressField,
1018
+ groups: (_proposal_groups = proposal.groups) != null ? _proposal_groups : null
732
1019
  }
733
1020
  ];
734
1021
  });