@aglyn/plugins-ai 1.0.0-beta.264 → 1.0.0-beta.268

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 (124) hide show
  1. package/package.json +18 -18
  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-job-site.d.ts +8 -1
  24. package/src/lib/components/ai-job-site.js +7 -0
  25. package/src/lib/components/ai-job-site.js.map +1 -1
  26. package/src/lib/components/ai-site-build-page.component.js +37 -0
  27. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  28. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  29. package/src/lib/components/ai-site-start-card.component.js +279 -240
  30. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  31. package/src/lib/components/ai-site-start-steps.component.d.ts +114 -0
  32. package/src/lib/components/ai-site-start-steps.component.js +480 -0
  33. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  34. package/src/lib/components/ai-site-start-tracking.d.ts +122 -0
  35. package/src/lib/components/ai-site-start-tracking.js +296 -0
  36. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  37. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  38. package/src/lib/components/ai-site-style-previews.js +115 -0
  39. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  40. package/src/lib/components/assist-jobs-drawer.component.js +24 -2
  41. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  42. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  43. package/src/lib/insights/ai-insight-digest.js +35 -11
  44. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  45. package/src/lib/jobs/ai-empty-site-sweep.d.ts +154 -0
  46. package/src/lib/jobs/ai-empty-site-sweep.js +346 -0
  47. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -0
  48. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  49. package/src/lib/jobs/ai-job-page-language.js +20 -2
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  52. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  53. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-site-datasets.d.ts +82 -1
  55. package/src/lib/jobs/ai-job-site-datasets.js +276 -14
  56. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  57. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -3
  58. package/src/lib/jobs/ai-jobs-beat.js +23 -6
  59. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  60. package/src/lib/jobs/ai-jobs.js +46 -7
  61. package/src/lib/jobs/ai-jobs.js.map +1 -1
  62. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -10
  63. package/src/lib/jobs/ai-layout-stock-photos.js +1046 -102
  64. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  65. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  66. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  67. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  68. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  69. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  70. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  71. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  72. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  73. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  74. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.d.ts +35 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js +379 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js.map +1 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.d.ts +31 -0
  78. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js +208 -0
  79. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js.map +1 -0
  80. package/src/lib/layout-language/ai-layout-compiler.js +49 -12
  81. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  82. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  83. package/src/lib/layout-language/ai-layout-design.js +19 -13
  84. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  85. package/src/lib/layout-language/ai-layout-listings.d.ts +48 -0
  86. package/src/lib/layout-language/ai-layout-listings.js +91 -5
  87. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  88. package/src/lib/layout-language/ai-layout-pictures.d.ts +15 -0
  89. package/src/lib/layout-language/ai-layout-pictures.js +6 -2
  90. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  91. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  92. package/src/lib/model/ai-jobs.types.js.map +1 -1
  93. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  94. package/src/lib/model/ai-site-home-arcs.js +741 -0
  95. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  96. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +9 -6
  99. package/src/lib/model/ai-site-job.js +8 -5
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-kinds.js +24 -24
  102. package/src/lib/model/ai-site-kinds.js.map +1 -1
  103. package/src/lib/model/ai-site-start-handoff.d.ts +49 -0
  104. package/src/lib/model/ai-site-start-handoff.js +88 -0
  105. package/src/lib/model/ai-site-start-handoff.js.map +1 -0
  106. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  107. package/src/lib/model/ai-site-start-steps.js +127 -0
  108. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  109. package/src/lib/model/ai-site-start.js +4 -1
  110. package/src/lib/model/ai-site-start.js.map +1 -1
  111. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  112. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  113. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  114. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  115. package/src/lib/runtime/ai-doctrine.js +9 -5
  116. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  117. package/src/lib/runtime/ai-palette.generated.js +5 -2
  118. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  119. package/src/lib/runtime/site-inventory.js +6 -0
  120. package/src/lib/runtime/site-inventory.js.map +1 -1
  121. package/src/lib/server/ai-insight-digest-route.js +2 -2
  122. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  123. package/src/lib/usage/assist-free-taste.js +3 -0
  124. 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';
@@ -175,6 +176,8 @@ export declare function aiSiteDatasetRecordPhotos(input: {
175
176
  name: string;
176
177
  /** Each record's name, by position. */
177
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>;
178
181
  pictures: AiSiteDatasetPictures;
179
182
  signal?: AbortSignal;
180
183
  stockPhotos?: typeof aiLayoutStockPhotoSource;
@@ -195,6 +198,23 @@ export interface AiSiteDatasetRunnerDeps {
195
198
  /** Where a pictured dataset's photos come from; the stock library, else the starters, otherwise. */
196
199
  photos?: typeof aiSiteDatasetRecordPhotos;
197
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;
198
218
  /**
199
219
  * The fields of a designed dataset that its records leave empty on most of
200
220
  * them — every record, or more than half — by the id the data plugin gave
@@ -249,6 +269,11 @@ export declare function aiDatasetListingsOf(datasets: ReadonlyArray<{
249
269
  imageField?: string | null;
250
270
  /** The record's page-address field, where the writer added one (AGL-3727). */
251
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;
252
277
  }>, screens: readonly AiLayoutListingScreen[]): AiLayoutListing[];
253
278
  /**
254
279
  * A `records` listing for each dataset the workspace already keeps that a
@@ -267,6 +292,62 @@ export declare function aiSiteReusedDatasetListings(input: {
267
292
  /** The listings the start already has, whose datasets are not listed twice. */
268
293
  listed: ReadonlyArray<Pick<AiLayoutListing, 'datasetId'>>;
269
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>;
270
351
  /**
271
352
  * The site's datasets a layout or a page unit is handed (AGL-3616), each a
272
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";
@@ -242,7 +244,9 @@ const limitReview = (message)=>({
242
244
  placed: null
243
245
  }));
244
246
  const people = input.pictures === 'people';
245
- 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({
246
250
  imageId: `record${index}`,
247
251
  frameId: null,
248
252
  iconId: null,
@@ -253,7 +257,14 @@ const limitReview = (message)=>({
253
257
  role: people ? 'about' : 'gallery',
254
258
  // A record's name is a search's words, never what its photo shows (AGL-3856).
255
259
  standIn: true
256
- }));
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
+ });
257
268
  const seed = `${job.$id}:records`;
258
269
  let found = [];
259
270
  try {
@@ -343,6 +354,51 @@ const limitReview = (message)=>({
343
354
  })
344
355
  });
345
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
+ }
346
402
  /**
347
403
  * The fields of a designed dataset that its records leave empty on most of
348
404
  * them — every record, or more than half — by the id the data plugin gave
@@ -371,7 +427,7 @@ const limitReview = (message)=>({
371
427
  function proposalOf(output) {
372
428
  var _output_proposal;
373
429
  const raw = (_output_proposal = output.proposal) != null ? _output_proposal : {};
374
- return {
430
+ return _extends({
375
431
  fields: Array.isArray(raw.fields) ? raw.fields.filter((field)=>field && typeof field.id === 'string' && field.id).map((field)=>({
376
432
  id: field.id,
377
433
  name: str(field.name) || field.id,
@@ -382,7 +438,12 @@ function proposalOf(output) {
382
438
  imageField: str(raw.imageField) || null,
383
439
  photos: Array.isArray(raw.photos) ? raw.photos.map(str).filter((src)=>src.startsWith('media:')).slice(0, 24) : [],
384
440
  sparseFields: Array.isArray(raw.sparseFields) ? raw.sparseFields.map(str).filter(Boolean) : []
385
- };
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
+ } : {});
386
447
  }
387
448
  /**
388
449
  * The dataset unit's runner, on the job the scaffold derived for it: the one
@@ -395,7 +456,7 @@ function proposalOf(output) {
395
456
  const writerFor = (_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter;
396
457
  const photosFor = (_deps_photos = deps.photos) != null ? _deps_photos : aiSiteDatasetRecordPhotos;
397
458
  return async (context)=>{
398
- 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;
399
460
  var _job_plan_create, _job_plan;
400
461
  const { job, firestore, signal } = context;
401
462
  const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, AI_DATASET_STEP)) != null ? _ref : aiModelForStep(AI_DATASET_STEP);
@@ -429,10 +490,10 @@ function proposalOf(output) {
429
490
  id: job.$id
430
491
  });
431
492
  if (written) {
432
- var _facts_fields2, _facts_addressField1, _facts_imageField;
493
+ var _facts_fields3, _facts_addressField1, _facts_imageField;
433
494
  const facts = written.facts;
434
495
  const again = output(written.id, written.name || creation.name, {
435
- fields: (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [],
496
+ fields: (_facts_fields3 = facts.fields) != null ? _facts_fields3 : [],
436
497
  recordNames: [],
437
498
  addressField: (_facts_addressField1 = facts.addressField) != null ? _facts_addressField1 : null,
438
499
  imageField: (_facts_imageField = facts.imageField) != null ? _facts_imageField : null
@@ -448,7 +509,7 @@ function proposalOf(output) {
448
509
  // A form's dataset (AGL-3616): its records are the submissions, so it is
449
510
  // designed from the plan with no model, and starts empty.
450
511
  if (input.forForm) {
451
- var _context_org1, _facts_fields3;
512
+ var _context_org1, _facts_fields4;
452
513
  const content = aiSiteFormDatasetContent(creation.name, creation.fields, input.forForm);
453
514
  const checked = keeper.writer.check(content, {
454
515
  hostId: job.hostId
@@ -485,7 +546,7 @@ function proposalOf(output) {
485
546
  return aiUnspentOutcome(model, {
486
547
  outputs: [
487
548
  _extends({}, output(result.id, result.name, {
488
- fields: (_facts_fields3 = facts.fields) != null ? _facts_fields3 : [],
549
+ fields: (_facts_fields4 = facts.fields) != null ? _facts_fields4 : [],
489
550
  recordNames: [],
490
551
  addressField: null
491
552
  }, 0), {
@@ -517,6 +578,8 @@ function proposalOf(output) {
517
578
  const designed = aiDatasetDraftContent(creation.name, generation.value, {
518
579
  recordPages: input.recordPages
519
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);
520
583
  // A list a visitor looks at keeps its records' photos (AGL-3616): searched
521
584
  // in code, never asked of the model, so they cost no credits.
522
585
  const photos = input.pictures ? await photosFor(_extends({
@@ -525,7 +588,10 @@ function proposalOf(output) {
525
588
  names: generation.value.records.map((values)=>{
526
589
  var _values_;
527
590
  return (_values_ = values[0]) != null ? _values_ : '';
528
- }),
591
+ })
592
+ }, category ? {
593
+ categories: category.values
594
+ } : {}, {
529
595
  pictures: input.pictures
530
596
  }, signal ? {
531
597
  signal
@@ -590,7 +656,7 @@ function proposalOf(output) {
590
656
  photos: placed
591
657
  } : {}, {
592
658
  sparseFields: aiDatasetSparseFields(generation.value, (_facts_fields1 = facts.fields) != null ? _facts_fields1 : [])
593
- });
659
+ }, aiSiteDatasetGroupsOf(category, (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [], generation.value));
594
660
  return _extends({}, spent, {
595
661
  outputs: [
596
662
  output(result.id, result.name, proposal, (_facts_records = facts.records) != null ? _facts_records : generation.value.records.length)
@@ -727,6 +793,201 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
727
793
  });
728
794
  return aiDatasetListingsOf(datasets, input.screens);
729
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
+ }
730
991
  /**
731
992
  * The site's datasets a layout or a page unit is handed (AGL-3616), each a
732
993
  * `records` listing with the sections of the plan's pages that name it, once
@@ -734,7 +995,7 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
734
995
  * nowhere: the pages built without it write the items out.
735
996
  */ export function aiSiteDatasetListings(input) {
736
997
  const made = input.datasets.flatMap((dataset)=>{
737
- var _proposal_imageField;
998
+ var _proposal_imageField, _proposal_groups;
738
999
  const id = input.delivered.get(dataset.name.trim().toLowerCase());
739
1000
  const output = id ? input.outputs.find((entry)=>entry.resource === 'draft' && entry.draftResource === AI_SITE_DATASET_RESOURCE && entry.id === id) : undefined;
740
1001
  if (!id || !output) return [];
@@ -753,7 +1014,8 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
753
1014
  return field.id !== proposal.addressField && field.id !== proposal.imageField && (index === 0 || !((_proposal_sparseFields = proposal.sparseFields) == null ? void 0 : _proposal_sparseFields.includes(field.id)));
754
1015
  }),
755
1016
  imageField: (_proposal_imageField = proposal.imageField) != null ? _proposal_imageField : null,
756
- addressField: proposal.addressField
1017
+ addressField: proposal.addressField,
1018
+ groups: (_proposal_groups = proposal.groups) != null ? _proposal_groups : null
757
1019
  }
758
1020
  ];
759
1021
  });