@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
@@ -38,12 +38,17 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
38
38
  * starter reads better than no picture when there is no library.
39
39
  *
40
40
  * Where the library answered and nothing in it fits a slot
41
- * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo
42
- * treatment — its frame with its icon, or its section's own band behind a
43
- * cover — and never a starter: the beta.257 Copper Kettle Catering start
44
- * (2026-10-15) opened a barbecue caterer's home page on the starters'
45
- * pottery workbench (AGL-3831). An empty picture draws nothing on the
46
- * published page, and the Besigner shows the owner where to choose one.
41
+ * ({@link AI_LAYOUT_NO_PHOTO}), the slot never takes a starter: the beta.257
42
+ * Copper Kettle Catering start (2026-10-15) opened a barbecue caterer's home
43
+ * page on the starters' pottery workbench (AGL-3831). Nor does it ship empty
44
+ * (AGL-3856): its picture is taken out, frame and all
45
+ * ({@link aiLayoutDropEmptyPictures}). A cover's frame is a black scrim laid
46
+ * over the whole section, and with nothing in it the beta.261 Synchronicity
47
+ * Gym hero read as a solid black block; a split's picture card is a rounded
48
+ * gray box, and the beta.262 Adult Hybrid Learning Center hero read as
49
+ * broken. Without them the section shows its own band, a cover's section
50
+ * still pinned to the site's dark scheme so the words written for a dark
51
+ * photo stay readable, and a split's words take the row's width.
47
52
  *
48
53
  * A slot keeps the alt text the model wrote only when it describes the photo
49
54
  * placed: a stock photo that is not what it says is described by what it is
@@ -83,6 +88,21 @@ export interface AiLayoutPictureSlot {
83
88
  * decorative once it is.
84
89
  */
85
90
  standIn?: boolean;
91
+ /**
92
+ * A portrait of one of the business's people (AGL-3856): a record of a
93
+ * team, staff or instructors list. Its photo shows an adult, in the role
94
+ * its words or its list name where one is listed, never a child or a thing.
95
+ */
96
+ people?: boolean;
97
+ /**
98
+ * A dataset record's cover (AGL-3856): what the record is filed under — the
99
+ * value of its list's category-like field ("Weddings and elopements") —
100
+ * read after its own name for what its photo is OF. A photographer's record
101
+ * is the photographed moment, never the photographer or the camera.
102
+ */
103
+ record?: {
104
+ category?: string;
105
+ };
86
106
  }
87
107
  /** A photo a slot is filled with. */
88
108
  export interface AiLayoutPicturePhoto {
@@ -178,6 +198,20 @@ export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlo
178
198
  * words. A slot given no photo is left as the compiler wrote it.
179
199
  */
180
200
  export declare function aiLayoutPlacePictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>): NodesMap;
201
+ /**
202
+ * The page without the pictures left with no photo (AGL-3856). A slot whose
203
+ * photo is missing loses its image, its icon and its frame where the frame
204
+ * holds nothing else, and so does every wrapper that holds nothing once it
205
+ * is gone, up to its section. A cover's black scrim over nothing read as a
206
+ * solid black block (the beta.261 Synchronicity Gym hero), and an empty
207
+ * rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning
208
+ * Center hero). A column of a split row taken out leaves the words' column
209
+ * the row's full width. A cover's section stays pinned to the site's dark
210
+ * scheme, which paints that scheme's background and text colors, so the
211
+ * words written for a dark photo stay readable. A slot a photo fills, a
212
+ * starter's included, is kept.
213
+ */
214
+ export declare function aiLayoutDropEmptyPictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>, sectionIds?: readonly string[]): NodesMap;
181
215
  /**
182
216
  * A source of photos found for the slots themselves — a stock search copied
183
217
  * into the site's library — answering per slot, in order: a photo; or
@@ -190,8 +224,8 @@ export type AiLayoutPictureSource = (slots: readonly AiLayoutPictureSlot[]) => P
190
224
  /**
191
225
  * Fills a stored page's empty picture slots (AGL-3660): from `source` where
192
226
  * one is given and it answers; a slot it answers with nothing fitting stays
193
- * without a photo; every other slot takes a starter photo, described by its
194
- * own words (AGL-3831). Never throws, and never leaves a page worse than it
227
+ * without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every
228
+ * other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it
195
229
  * came: a picture is decoration, and a job never fails over one.
196
230
  */
197
231
  export declare function aiResolveLayoutPictures(nodes: NodesMap, input: {
@@ -196,8 +196,12 @@ function rotate(list, by) {
196
196
  ] : rotate(STARTER_POOLS[slot.role], turn);
197
197
  const rest = rotate(ALL_STARTERS.filter((name)=>!pool.includes(name)), turn);
198
198
  // The owner's portrait is a picture of a person, and only a picture of
199
- // people takes it: never a vase in a gallery (AGL-3660).
200
- const candidates = [
199
+ // people takes it: never a vase in a gallery (AGL-3660). A portrait of
200
+ // one of the business's people takes only people, repeated before a
201
+ // workbench stands in for a teacher (AGL-3856).
202
+ const candidates = slot.people ? [
203
+ ...STARTER_POOLS.about
204
+ ] : [
201
205
  ...pool,
202
206
  ...rest
203
207
  ].filter((name)=>slot.role === 'about' || name !== 'about');
@@ -271,11 +275,91 @@ function rotate(list, by) {
271
275
  });
272
276
  return next;
273
277
  }
278
+ /** Whether a node is a grid row: a `muiGrid` container. */ const isGridRow = (node)=>{
279
+ var _node_props;
280
+ return (node == null ? void 0 : node['componentId']) === 'muiGrid' && ((_node_props = node['props']) == null ? void 0 : _node_props['container']) === true;
281
+ };
282
+ /** Whether a node is a column of a grid row: a sized `muiGrid` item. */ const isGridColumn = (node)=>(node == null ? void 0 : node['componentId']) === 'muiGrid' && !isGridRow(node);
283
+ /**
284
+ * The page without the pictures left with no photo (AGL-3856). A slot whose
285
+ * photo is missing loses its image, its icon and its frame where the frame
286
+ * holds nothing else, and so does every wrapper that holds nothing once it
287
+ * is gone, up to its section. A cover's black scrim over nothing read as a
288
+ * solid black block (the beta.261 Synchronicity Gym hero), and an empty
289
+ * rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning
290
+ * Center hero). A column of a split row taken out leaves the words' column
291
+ * the row's full width. A cover's section stays pinned to the site's dark
292
+ * scheme, which paints that scheme's background and text colors, so the
293
+ * words written for a dark photo stay readable. A slot a photo fills, a
294
+ * starter's included, is kept.
295
+ */ export function aiLayoutDropEmptyPictures(nodes, slots, photos, sectionIds = []) {
296
+ const map = nodes;
297
+ const empty = slots.filter((slot, index)=>{
298
+ var _photos_index, _map_slot_imageId_props, _map_slot_imageId;
299
+ if ((_photos_index = photos[index]) == null ? void 0 : _photos_index.src) return false;
300
+ const src = (_map_slot_imageId = map[slot.imageId]) == null ? void 0 : (_map_slot_imageId_props = _map_slot_imageId['props']) == null ? void 0 : _map_slot_imageId_props['src'];
301
+ return Boolean(map[slot.imageId]) && !(typeof src === 'string' && src.trim());
302
+ });
303
+ if (!empty.length) return nodes;
304
+ const next = _extends({}, map);
305
+ const children = (id)=>{
306
+ var _ref;
307
+ var _next_id;
308
+ return (_ref = (_next_id = next[id]) == null ? void 0 : _next_id['nodes']) != null ? _ref : [];
309
+ };
310
+ const parentOf = (id)=>{
311
+ var _Object_keys_find;
312
+ return (_Object_keys_find = Object.keys(next).find((key)=>children(key).includes(id))) != null ? _Object_keys_find : null;
313
+ };
314
+ const remove = (id)=>{
315
+ const parent = parentOf(id);
316
+ if (parent) next[parent] = _extends({}, next[parent], {
317
+ nodes: children(parent).filter((child)=>child !== id)
318
+ });
319
+ delete next[id];
320
+ return parent;
321
+ };
322
+ const sections = new Set(sectionIds);
323
+ for (const slot of empty){
324
+ const own = new Set([
325
+ slot.imageId,
326
+ slot.iconId
327
+ ].filter(Boolean));
328
+ const frameHoldsOnly = Boolean(slot.frameId && next[slot.frameId] && children(slot.frameId).every((child)=>own.has(child)));
329
+ let parent = remove(slot.imageId);
330
+ if (slot.iconId && next[slot.iconId]) remove(slot.iconId);
331
+ if (!frameHoldsOnly) continue;
332
+ // The frame, then each wrapper it leaves empty: never a section, nor the page's root.
333
+ let at = slot.frameId;
334
+ while(at && next[at] && !children(at).length && !sections.has(at) && parentOf(at) !== null){
335
+ const column = isGridColumn(next[at]);
336
+ parent = remove(at);
337
+ at = column ? null : parent;
338
+ }
339
+ // A split row left with one column gives its words the row's width.
340
+ if (parent && isGridRow(next[parent])) {
341
+ const [only, ...rest] = children(parent);
342
+ if (only && !rest.length && isGridColumn(next[only])) {
343
+ var _column_sx, _column_props;
344
+ const column = next[only];
345
+ const sx = _extends({}, (_column_sx = column['sx']) != null ? _column_sx : {});
346
+ delete sx['order'];
347
+ next[only] = _extends({}, column, {
348
+ props: _extends({}, (_column_props = column['props']) != null ? _column_props : {}, {
349
+ size: 'xs:12'
350
+ }),
351
+ sx: Object.keys(sx).length ? sx : null
352
+ });
353
+ }
354
+ }
355
+ }
356
+ return next;
357
+ }
274
358
  /**
275
359
  * Fills a stored page's empty picture slots (AGL-3660): from `source` where
276
360
  * one is given and it answers; a slot it answers with nothing fitting stays
277
- * without a photo; every other slot takes a starter photo, described by its
278
- * own words (AGL-3831). Never throws, and never leaves a page worse than it
361
+ * without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every
362
+ * other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it
279
363
  * came: a picture is decoration, and a job never fails over one.
280
364
  */ export async function aiResolveLayoutPictures(nodes, input) {
281
365
  try {
@@ -301,12 +385,13 @@ function rotate(list, by) {
301
385
  const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo)=>otherLanguage ? _extends({}, photo, {
302
386
  alt: aiLayoutLabel('illustrativePhoto', input.language)
303
387
  }) : photo);
304
- return aiLayoutPlacePictures(nodes, slots, slots.map((_slot, index)=>{
388
+ const photos = slots.map((_slot, index)=>{
305
389
  var _starters_index;
306
390
  const answer = found[index];
307
391
  if (answer) return answer.src ? answer : null;
308
392
  return (_starters_index = starters[index]) != null ? _starters_index : null;
309
- }));
393
+ });
394
+ return aiLayoutDropEmptyPictures(aiLayoutPlacePictures(nodes, slots, photos), slots, photos, input.sectionIds);
310
395
  } catch (error) {
311
396
  console.error('ai layout pictures: the page keeps its empty picture slots', {
312
397
  error
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot keeps the compiler's no-photo\n * treatment — its frame with its icon, or its section's own band behind a\n * cover — and never a starter: the beta.257 Copper Kettle Catering start\n * (2026-10-15) opened a barbecue caterer's home page on the starters'\n * pottery workbench (AGL-3831). An empty picture draws nothing on the\n * published page, and the Besigner shows the owner where to choose one.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo; every other slot takes a starter photo, described by its\n * own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n }),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutLabel, aiLayoutSpeaksOtherThanEnglish } from './ai-layout-labels'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Where the library cannot be asked at all — no stock-photo key, the\n * library down, refusing or out of time before it answered — one of the\n * STARTER PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`,\n * served at `/_static/starter/` by the tenant and the console alike,\n * credited in the CREDITS file beside them). They are the platform's own\n * files, so a visitor's browser asks no other host for them, and a\n * starter reads better than no picture when there is no library.\n *\n * Where the library answered and nothing in it fits a slot\n * ({@link AI_LAYOUT_NO_PHOTO}), the slot never takes a starter: the beta.257\n * Copper Kettle Catering start (2026-10-15) opened a barbecue caterer's home\n * page on the starters' pottery workbench (AGL-3831). Nor does it ship empty\n * (AGL-3856): its picture is taken out, frame and all\n * ({@link aiLayoutDropEmptyPictures}). A cover's frame is a black scrim laid\n * over the whole section, and with nothing in it the beta.261 Synchronicity\n * Gym hero read as a solid black block; a split's picture card is a rounded\n * gray box, and the beta.262 Adult Hybrid Learning Center hero read as\n * broken. Without them the section shows its own band, a cover's section\n * still pinned to the site's dark scheme so the words written for a dark\n * photo stay readable, and a split's words take the row's width.\n *\n * A slot keeps the alt text the model wrote only when it describes the photo\n * placed: a stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`), and a starter photo always by its own words, since\n * the model never wrote for it (AGL-3831). A stand-in the compiler drew where\n * the model described no picture is decorative while empty; the photo placed\n * there is described by its own words and is no longer decorative (AGL-3856).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n /**\n * A stand-in the compiler drew where the model described no picture\n * (AGL-3856): its words are what a search looks for, never what a photo\n * shows, so a photo placed there is described by its own, and is no longer\n * decorative once it is.\n */\n standIn?: boolean\n /**\n * A portrait of one of the business's people (AGL-3856): a record of a\n * team, staff or instructors list. Its photo shows an adult, in the role\n * its words or its list name where one is listed, never a child or a thing.\n */\n people?: boolean\n /**\n * A dataset record's cover (AGL-3856): what the record is filed under — the\n * value of its list's category-like field (\"Weddings and elopements\") —\n * read after its own name for what its photo is OF. A photographer's record\n * is the photographed moment, never the photographer or the camera.\n */\n record?: { category?: string }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n ...(node.props?.['decorative'] === true ? { standIn: true } : {}),\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name, each with the words that describe it. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\n/**\n * A source's answer for a slot the library answered and nothing in it fits\n * (AGL-3831): the slot keeps its no-photo treatment rather than take a\n * starter. Its source is empty, so a caller reading `src` to fill something\n * that must hold a photo (a product's, a record's) falls back as it would on\n * `null`.\n */\nexport const AI_LAYOUT_NO_PHOTO: Readonly<AiLayoutPicturePhoto> = Object.freeze({ src: '', width: 0, height: 0 })\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660). A portrait of\n // one of the business's people takes only people, repeated before a\n // workbench stands in for a teacher (AGL-3856).\n const candidates = slot.people\n ? [...STARTER_POOLS.about]\n : [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height, alt: photo.alt }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n // A photo placed is described by its words, never hidden from a screen\n // reader: a stand-in was decorative only while it showed nothing (AGL-3856).\n if (photo.alt?.trim()) delete props['decorative']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/** Whether a node is a grid row: a `muiGrid` container. */\nconst isGridRow = (node: Record<string, unknown> | undefined) =>\n node?.['componentId'] === 'muiGrid' && (node['props'] as Record<string, unknown> | undefined)?.['container'] === true\n\n/** Whether a node is a column of a grid row: a sized `muiGrid` item. */\nconst isGridColumn = (node: Record<string, unknown> | undefined) =>\n node?.['componentId'] === 'muiGrid' && !isGridRow(node)\n\n/**\n * The page without the pictures left with no photo (AGL-3856). A slot whose\n * photo is missing loses its image, its icon and its frame where the frame\n * holds nothing else, and so does every wrapper that holds nothing once it\n * is gone, up to its section. A cover's black scrim over nothing read as a\n * solid black block (the beta.261 Synchronicity Gym hero), and an empty\n * rounded gray card as a broken picture (the beta.262 Adult Hybrid Learning\n * Center hero). A column of a split row taken out leaves the words' column\n * the row's full width. A cover's section stays pinned to the site's dark\n * scheme, which paints that scheme's background and text colors, so the\n * words written for a dark photo stay readable. A slot a photo fills, a\n * starter's included, is kept.\n */\nexport function aiLayoutDropEmptyPictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n sectionIds: readonly string[] = [],\n): NodesMap {\n const map = nodes as unknown as Record<string, Record<string, unknown>>\n const empty = slots.filter((slot, index) => {\n if (photos[index]?.src) return false\n const src = (map[slot.imageId]?.['props'] as Record<string, unknown> | undefined)?.['src']\n return Boolean(map[slot.imageId]) && !(typeof src === 'string' && src.trim())\n })\n if (!empty.length) return nodes\n const next = { ...map }\n const children = (id: string) => (next[id]?.['nodes'] as string[] | undefined) ?? []\n const parentOf = (id: string) => Object.keys(next).find((key) => children(key).includes(id)) ?? null\n const remove = (id: string) => {\n const parent = parentOf(id)\n if (parent) next[parent] = { ...next[parent], nodes: children(parent).filter((child) => child !== id) }\n delete next[id]\n return parent\n }\n const sections = new Set(sectionIds)\n for (const slot of empty) {\n const own = new Set([slot.imageId, slot.iconId].filter(Boolean) as string[])\n const frameHoldsOnly = Boolean(slot.frameId && next[slot.frameId] && children(slot.frameId).every((child) => own.has(child)))\n let parent = remove(slot.imageId)\n if (slot.iconId && next[slot.iconId]) remove(slot.iconId)\n if (!frameHoldsOnly) continue\n // The frame, then each wrapper it leaves empty: never a section, nor the page's root.\n let at: string | null = slot.frameId\n while (at && next[at] && !children(at).length && !sections.has(at) && parentOf(at) !== null) {\n const column = isGridColumn(next[at])\n parent = remove(at)\n at = column ? null : parent\n }\n // A split row left with one column gives its words the row's width.\n if (parent && isGridRow(next[parent])) {\n const [only, ...rest] = children(parent)\n if (only && !rest.length && isGridColumn(next[only])) {\n const column = next[only] as Record<string, unknown>\n const sx = { ...((column['sx'] as Record<string, unknown> | undefined) ?? {}) }\n delete sx['order']\n next[only] = {\n ...column,\n props: { ...((column['props'] as Record<string, unknown> | undefined) ?? {}), size: 'xs:12' },\n sx: Object.keys(sx).length ? sx : null,\n }\n }\n }\n }\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering per slot, in order: a photo; or\n * {@link AI_LAYOUT_NO_PHOTO} where the library answered and nothing fits,\n * which leaves the slot without a photo; or `null` where the library could\n * not be asked, which takes a starter photo. It may throw, and then every\n * slot takes a starter.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers; a slot it answers with nothing fitting stays\n * without a photo and is taken out ({@link aiLayoutDropEmptyPictures}); every\n * other slot takes a starter photo, described by its own words (AGL-3831). Never throws, and never leaves a page worse than it\n * came: a picture is decoration, and a job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n /** The site's language (AGL-3750); a starter photo's English words are not its own. */\n language?: string\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n // The starters' words are English; a site in another language says it\n // shows an illustrative photo, true of any photo, as a stock photo's does.\n const otherLanguage = aiLayoutSpeaksOtherThanEnglish(input.language)\n const starters = aiLayoutStarterPhotos(slots, input.seed).map((photo) =>\n otherLanguage ? { ...photo, alt: aiLayoutLabel('illustrativePhoto', input.language) } : photo,\n )\n const photos = slots.map((_slot, index) => {\n const answer = found[index]\n if (answer) return answer.src ? answer : null\n return starters[index] ?? null\n })\n return aiLayoutDropEmptyPictures(aiLayoutPlacePictures(nodes, slots, photos), slots, photos, input.sectionIds)\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","aiLayoutLabel","aiLayoutSpeaksOtherThanEnglish","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","standIn","role","names","AI_LAYOUT_STARTER_PHOTOS","AI_LAYOUT_NO_PHOTO","Object","freeze","width","height","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","people","candidate","beside","Boolean","get","set","photo","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","isGridRow","isGridColumn","aiLayoutDropEmptyPictures","sectionIds","empty","children","parentOf","key","remove","parent","frameHoldsOnly","every","at","column","only","size","aiResolveLayoutPictures","sectionNames","found","source","error","console","warn","otherLanguage","language","starters","_slot","answer"],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A site start's HOME ARC (AGL-3899): which sections its home page tells
19
+ * its story with, and in what order, chosen in code by the job's seed among
20
+ * the arcs that suit the site's kind.
21
+ *
22
+ * Why. Told one outline for every home ("a hero, then services, about or why
23
+ * us, … a closing call to action last"), the 22 newest production starts of
24
+ * 2026-10-16 were 16 times hero → offer → about → proof → closing call to
25
+ * action, and 19 of them ended on a call to action. Zach: "almost every AI
26
+ * site seems to look the same … they all have the same sections". A theme
27
+ * that varies on a fixed outline still reads as one site.
28
+ *
29
+ * So each kind has a few arcs, each a different way a designer would tell
30
+ * that business's story — the work first, the story first, the steps first,
31
+ * a close on the questions people ask, a gallery of the place, a menu or a
32
+ * services list where the kind has one — and the seed picks one. The plan's
33
+ * turn names it in one line; the model still names, writes and designs every
34
+ * section, and the plan rules hold it as they hold any plan.
35
+ *
36
+ * Every arc is a hero and four sections: the home's fewest
37
+ * (`AI_SITE_HOME_MIN_SECTIONS`), which the Free wall pays for beside the
38
+ * other page, and which a paid plan adds to where the brief gives more. A
39
+ * store's arcs are its storefront's parts (`AI_SITE_STORE_HOME_BANDS`) in
40
+ * three orders, the newsletter sign-up always last, as the storefront settle
41
+ * keeps it (`aiStorefrontPlanScreens`). No
42
+ * arc promises what a new site has not got: figures, prices, listings and
43
+ * shows are "only as the brief gives them", and none plans what customers
44
+ * say (AGL-3887).
45
+ *
46
+ * Pure data and one reader, safe for the console to import.
47
+ */
48
+ /** What a section of an arc is for: what the variety specs compare arcs by. */
49
+ export type AiSiteHomeRole = 'offer' | 'work' | 'story' | 'steps' | 'approach' | 'why' | 'faq' | 'gallery' | 'menu' | 'people' | 'area' | 'facts' | 'listen' | 'writing' | 'topics' | 'cta' | 'products' | 'collections' | 'reviews' | 'signup';
50
+ /** One section of an arc: its role, and the words the plan is told. */
51
+ export interface AiSiteHomeArcSection {
52
+ role: AiSiteHomeRole;
53
+ words: string;
54
+ }
55
+ /** A home arc: an id the job records, and the sections after the hero, in order. */
56
+ export interface AiSiteHomeArc {
57
+ id: string;
58
+ sections: readonly AiSiteHomeArcSection[];
59
+ }
60
+ /** The arcs a kind's home may follow. */
61
+ export declare function aiSiteHomeArcsFor(kind: string | null | undefined): readonly AiSiteHomeArc[];
62
+ /**
63
+ * The arc a job's home follows: one of its kind's, picked by the job's seed
64
+ * (`aiSiteSeed` of the job id) under a salt of its own, so the arc is drawn
65
+ * apart from the look and the compiler's choices, which read the same seed.
66
+ */
67
+ export declare function aiSiteHomeArc(input: {
68
+ kind: string | null | undefined;
69
+ seed: number;
70
+ }): AiSiteHomeArc;
71
+ /**
72
+ * The plan's one line for its arc: the hero first, then the arc's sections in
73
+ * its order, more only where the brief gives them and the plan has room.
74
+ */
75
+ export declare function aiSiteHomeArcSentence(arc: AiSiteHomeArc): string;