@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.239

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 (172) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
  3. package/src/lib/components/ai-credits-prompt.component.js +84 -34
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-links.js +8 -0
  9. package/src/lib/components/ai-job-links.js.map +1 -1
  10. package/src/lib/components/ai-job-plan.component.js +12 -1
  11. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  12. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  13. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  14. package/src/lib/components/ai-jobs-store.d.ts +12 -0
  15. package/src/lib/components/ai-jobs-store.js +15 -0
  16. package/src/lib/components/ai-jobs-store.js.map +1 -1
  17. package/src/lib/components/ai-site-build-page.component.js +22 -2
  18. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  19. package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
  20. package/src/lib/components/ai-usage-meter-refresh.js +63 -0
  21. package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
  22. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  23. package/src/lib/components/ai-usage-strip.component.js +40 -9
  24. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  25. package/src/lib/components/assist-jobs-drawer.component.js +12 -16
  26. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  27. package/src/lib/components/assist-panel.component.js +91 -51
  28. package/src/lib/components/assist-panel.component.js.map +1 -1
  29. package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
  30. package/src/lib/components/use-ai-usage-meter.js +8 -3
  31. package/src/lib/components/use-ai-usage-meter.js.map +1 -1
  32. package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
  33. package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
  34. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  35. package/src/lib/jobs/ai-job-build-step.js +33 -3
  36. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  38. package/src/lib/jobs/ai-job-cancel.js +102 -0
  39. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  40. package/src/lib/jobs/ai-job-component-step.js +4 -1
  41. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-draft-ids.js +7 -2
  43. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  44. package/src/lib/jobs/ai-job-drafts.js +2 -0
  45. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  46. package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
  47. package/src/lib/jobs/ai-job-form-dataset.js +161 -0
  48. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
  49. package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
  50. package/src/lib/jobs/ai-job-form-step.js +41 -10
  51. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  53. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  54. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  55. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  56. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  58. package/src/lib/jobs/ai-job-page-language.js +45 -2
  59. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  60. package/src/lib/jobs/ai-job-page-step.js +12 -1
  61. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
  63. package/src/lib/jobs/ai-job-plan-step.js +90 -13
  64. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
  66. package/src/lib/jobs/ai-job-site-content.js +66 -7
  67. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  68. package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
  69. package/src/lib/jobs/ai-job-site-datasets.js +499 -0
  70. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
  71. package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
  72. package/src/lib/jobs/ai-job-site-step.js +152 -30
  73. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  74. package/src/lib/jobs/ai-job-template-step.js +4 -1
  75. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  76. package/src/lib/jobs/ai-jobs.d.ts +37 -12
  77. package/src/lib/jobs/ai-jobs.js +184 -28
  78. package/src/lib/jobs/ai-jobs.js.map +1 -1
  79. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
  80. package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
  81. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  82. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  83. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  84. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  85. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
  86. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
  87. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  90. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  91. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
  92. package/src/lib/layout-language/ai-layout-compiler.js +397 -9
  93. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  95. package/src/lib/layout-language/ai-layout-design.js +5 -3
  96. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  98. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  99. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js +17 -1
  101. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  102. package/src/lib/layout-language/ai-layout-listings.d.ts +44 -3
  103. package/src/lib/layout-language/ai-layout-listings.js +118 -50
  104. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  105. package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
  106. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  107. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  108. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  109. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  110. package/src/lib/model/ai-build-job.d.ts +11 -1
  111. package/src/lib/model/ai-build-job.js +35 -6
  112. package/src/lib/model/ai-build-job.js.map +1 -1
  113. package/src/lib/model/ai-build-plan.d.ts +23 -3
  114. package/src/lib/model/ai-build-plan.js +47 -6
  115. package/src/lib/model/ai-build-plan.js.map +1 -1
  116. package/src/lib/model/ai-build-progress.d.ts +2 -2
  117. package/src/lib/model/ai-build-progress.js +6 -4
  118. package/src/lib/model/ai-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-credit-estimate.d.ts +69 -2
  120. package/src/lib/model/ai-credit-estimate.js +91 -3
  121. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  122. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  123. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  124. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  125. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  126. package/src/lib/model/ai-job-failure-copy.js +8 -3
  127. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  128. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  129. package/src/lib/model/ai-jobs.types.js.map +1 -1
  130. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  131. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  132. package/src/lib/model/ai-site-build-progress.js +7 -4
  133. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-site-job.d.ts +38 -4
  135. package/src/lib/model/ai-site-job.js +50 -14
  136. package/src/lib/model/ai-site-job.js.map +1 -1
  137. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  138. package/src/lib/model/ai-site-kinds.js +34 -6
  139. package/src/lib/model/ai-site-kinds.js.map +1 -1
  140. package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
  141. package/src/lib/runtime/ai-dataset-generation.js +127 -0
  142. package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
  143. package/src/lib/runtime/ai-doctrine-validators.d.ts +77 -1
  144. package/src/lib/runtime/ai-doctrine-validators.js +280 -6
  145. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  146. package/src/lib/runtime/ai-doctrine.js +10 -4
  147. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  148. package/src/lib/runtime/ai-node-tree.d.ts +2 -0
  149. package/src/lib/runtime/ai-node-tree.js +16 -0
  150. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  151. package/src/lib/runtime/ai-palette.generated.js +53 -7
  152. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  153. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  154. package/src/lib/server/ai-jobs-cancel.js +27 -9
  155. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  156. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  157. package/src/lib/server/ai-jobs-gate.js +20 -9
  158. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  159. package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
  160. package/src/lib/tools/ai-dataset-tool.js +231 -0
  161. package/src/lib/tools/ai-dataset-tool.js.map +1 -0
  162. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  163. package/src/lib/usage/ai-usage-meter.js +17 -1
  164. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  165. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  166. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  167. package/src/lib/usage/assist-usage.d.ts +7 -0
  168. package/src/lib/usage/assist-usage.js +14 -4
  169. package/src/lib/usage/assist-usage.js.map +1 -1
  170. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  171. package/src/lib/usage/free-ai-credits-left.js +30 -4
  172. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -44,6 +44,22 @@
44
44
  * music site, placed EMPTY: its "add your tracks" state, for the owner to
45
45
  * fill from their own media library. No job ever sources a recording, so
46
46
  * a tracks listing has no records and the player no source.
47
+ * - `reviews` — a selling store's own customer reviews, through the commerce
48
+ * plugin's Product reviews in its store-wide scope (`product-reviews`,
49
+ * AGL-3676): approved reviews across the catalog with their star ratings.
50
+ * A store that opens has none, and the element then shows nothing at all
51
+ * on the published page, so no review is ever written for a shopper who
52
+ * did not leave one (the FTC's fake-review rule; rule 14's "never invent
53
+ * a customer").
54
+ * - `signup` — a selling store's newsletter sign-up, through the commerce
55
+ * plugin's Newsletter signup (`newsletter-signup`, AGL-3676): a consented
56
+ * email field into the store's contacts, where a section says newsletter.
57
+ * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its
58
+ * services, built by the site start and kept by the data plugin. Drawn as
59
+ * a Box that repeats over the dataset (`repeatDataset`, the platform's own
60
+ * repeat), its card binding the record's fields as `{{item.<field>}}`, in
61
+ * the sections whose plan names the dataset in its `uses`. A site may list
62
+ * several, one listing a dataset.
47
63
  *
48
64
  * The elements are named by their persisted ids, as the form binding names
49
65
  * `form`: a plugin never imports another's internals, and
@@ -52,7 +68,7 @@
52
68
  *
53
69
  * Model only: shapes and the inputs reader; the compiler draws them.
54
70
  */
55
- export type AiLayoutListingKind = 'products' | 'posts' | 'tracks';
71
+ export type AiLayoutListingKind = 'products' | 'posts' | 'tracks' | 'reviews' | 'signup' | 'records';
56
72
  /** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */
57
73
  export type AiLayoutListingRole = 'featured' | 'index';
58
74
  /** One section of one page that places a listing. */
@@ -63,6 +79,14 @@ export interface AiLayoutListingPlacement {
63
79
  section: number;
64
80
  role: AiLayoutListingRole;
65
81
  }
82
+ /** One field of a dataset a `records` listing shows, as its card binds it. */
83
+ export interface AiLayoutListingField {
84
+ /** The field's id, which `{{item.<id>}}` reads. */
85
+ id: string;
86
+ name: string;
87
+ /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */
88
+ type: string;
89
+ }
66
90
  /** A kind of record the site keeps, and where its pages place it. */
67
91
  export interface AiLayoutListing {
68
92
  /** `listing:products`, `listing:posts`: what a section's placement names. */
@@ -76,6 +100,10 @@ export interface AiLayoutListing {
76
100
  href?: string;
77
101
  /** The content collection a posts listing repeats, by its slug. */
78
102
  collectionSlug?: string;
103
+ /** The dataset a records listing repeats over, by id (AGL-3616). */
104
+ datasetId?: string;
105
+ /** The dataset's fields, in its order, which the card binds. */
106
+ fields?: AiLayoutListingField[];
79
107
  /**
80
108
  * Where a visitor goes from a store with nothing listed yet (AGL-3676): the
81
109
  * site's contact page, by its path. The empty state names it as its one
@@ -94,8 +122,8 @@ export interface AiLayoutListing {
94
122
  }
95
123
  /** The unit input a site's listings travel in, to its layout and its pages. */
96
124
  export declare const AI_LAYOUT_LISTINGS_INPUT = "siteListings";
97
- /** The id a listing of a kind is placed by. */
98
- export declare function aiLayoutListingId(kind: AiLayoutListingKind): string;
125
+ /** The id a listing of a kind is placed by; a records listing is one dataset's. */
126
+ export declare function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string;
99
127
  /** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */
100
128
  export declare const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>>;
101
129
  /** The header's cart button: the commerce plugin's Cart in its `button` variant. */
@@ -105,6 +133,9 @@ export declare const AI_LAYOUT_FEATURED_RECORDS: {
105
133
  readonly products: 4;
106
134
  readonly posts: 3;
107
135
  readonly tracks: 0;
136
+ readonly reviews: 3;
137
+ readonly signup: 0;
138
+ readonly records: 3;
108
139
  };
109
140
  /** A site's listings as its unit inputs carry them; nothing that does not read is kept. */
110
141
  export declare function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[];
@@ -159,3 +190,13 @@ export declare function aiLayoutIsShopPage(screen: {
159
190
  * A section placed once is placed for one listing.
160
191
  */
161
192
  export declare function aiLayoutListingPlacements(kind: AiLayoutListingKind, screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
193
+ /**
194
+ * The sections that list one of the site's datasets (AGL-3616): every section
195
+ * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,
196
+ * or its id once built. The home page features a few of its records; any
197
+ * other page lists them all. Chosen from the plan, never asked of the model.
198
+ */
199
+ export declare function aiLayoutRecordsPlacements(dataset: {
200
+ id: string;
201
+ name: string;
202
+ }, screens: readonly AiLayoutListingScreen[]): AiLayoutListingPlacement[];
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -13,58 +14,28 @@
13
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
- */ /**
17
- * A SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).
18
- *
19
- * A guided start writes a store's first products and a blog's first posts,
20
- * then built the pages around them in the layout language — and the pages
21
- * showed neither. "Featured candles" and "The candle range" on the live
22
- * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a
23
- * line each: no photo, no price, no link, nothing a shopper could open; the
24
- * Clay Notes blog's "Featured writing" was three cards that linked to no
25
- * post. The real records were on the site, one click away from nowhere.
26
- *
27
- * A LISTING is one kind of record a site keeps — its catalog, its blog —
28
- * placed by code in the sections whose plan says they show it, through the
29
- * element the plugin that keeps the records renders them with, bound to the
30
- * records themselves rather than to words about them. It is the form binding
31
- * one step on (a section whose plan `uses` a form places that form by its
32
- * id, whatever the answer remembered): the model writes the section's
33
- * heading and words, the platform places the records.
34
- *
35
- * - `products` — the commerce plugin's Product grid (`product-grid`), over
36
- * the site's whole active catalog: a photo, the name, the price (or
37
- * "Price coming soon" where the owner cleared it), each card linking its
38
- * product's page.
39
- * - `posts` — the content plugin's Collection Entries (`collectionEntries`)
40
- * over the blog's collection, its card a cover, the date and byline, the
41
- * title and the excerpt, linking each post.
42
- * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a
43
- * music site, placed EMPTY: its "add your tracks" state, for the owner to
44
- * fill from their own media library. No job ever sources a recording, so
45
- * a tracks listing has no records and the player no source.
46
- *
47
- * The elements are named by their persisted ids, as the form binding names
48
- * `form`: a plugin never imports another's internals, and
49
- * `ai-layout-listings.spec.ts` holds the ids and props named here to the
50
- * source of the plugins that declare them.
51
- *
52
- * Model only: shapes and the inputs reader; the compiler draws them.
53
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ */ import { aiStorefrontSays } from "./ai-layout-storefront.js";
54
18
  /** The unit input a site's listings travel in, to its layout and its pages. */ export const AI_LAYOUT_LISTINGS_INPUT = 'siteListings';
55
- /** The id a listing of a kind is placed by. */ export function aiLayoutListingId(kind) {
56
- return `listing:${kind}`;
19
+ /** The id a listing of a kind is placed by; a records listing is one dataset's. */ export function aiLayoutListingId(kind, datasetId) {
20
+ return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `listing:${kind}`;
57
21
  }
58
22
  /** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */ export const AI_LAYOUT_LISTING_ELEMENTS = {
59
23
  products: 'product-grid',
60
24
  posts: 'collectionEntries',
61
- tracks: 'musicPlayer'
25
+ tracks: 'musicPlayer',
26
+ reviews: 'product-reviews',
27
+ signup: 'newsletter-signup',
28
+ // The platform's own Box, repeating its card over the dataset.
29
+ records: 'muiBox'
62
30
  };
63
31
  /** The header's cart button: the commerce plugin's Cart in its `button` variant. */ export const AI_LAYOUT_CART_ELEMENT = 'cart';
64
32
  /** The most records a featured band shows; a page of its own shows them all. */ export const AI_LAYOUT_FEATURED_RECORDS = {
65
33
  products: 4,
66
34
  posts: 3,
67
- tracks: 0
35
+ tracks: 0,
36
+ reviews: 3,
37
+ signup: 0,
38
+ records: 3
68
39
  };
69
40
  const PLACEMENT_ROLES = [
70
41
  'featured',
@@ -73,10 +44,37 @@ const PLACEMENT_ROLES = [
73
44
  const KINDS = [
74
45
  'products',
75
46
  'posts',
76
- 'tracks'
47
+ 'tracks',
48
+ 'reviews',
49
+ 'signup',
50
+ 'records'
77
51
  ];
78
52
  const text = (value)=>typeof value === 'string' ? value.trim() : '';
53
+ /** What a visitor calls each kind's whole list, where a unit's input names none. */ const LISTING_NAMES = {
54
+ products: 'the shop',
55
+ posts: 'the blog',
56
+ tracks: 'the music',
57
+ reviews: 'the store',
58
+ signup: 'the newsletter',
59
+ records: 'the list'
60
+ };
79
61
  /** A path on the site, as a listing's links are kept. */ const SITE_PATH = /^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/;
62
+ /** A dataset id and a field id, as the data plugin mints them. */ const DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/;
63
+ const FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
64
+ /** A records listing's fields as its input carries them; nothing that does not read is kept. */ function listingFieldsOf(raw) {
65
+ if (!Array.isArray(raw)) return [];
66
+ return raw.flatMap((value)=>{
67
+ const field = value != null ? value : {};
68
+ const id = text(field['id']);
69
+ return FIELD_ID.test(id) ? [
70
+ {
71
+ id,
72
+ name: text(field['name']).slice(0, 60) || id,
73
+ type: text(field['type']) || 'text'
74
+ }
75
+ ] : [];
76
+ }).slice(0, 16);
77
+ }
80
78
  /** A site's listings as its unit inputs carry them; nothing that does not read is kept. */ export function aiLayoutListingsOf(inputs) {
81
79
  const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_LISTINGS_INPUT];
82
80
  if (!Array.isArray(raw)) return [];
@@ -86,7 +84,14 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
86
84
  if (!entry || typeof entry !== 'object') continue;
87
85
  const record = entry;
88
86
  const kind = record['kind'];
89
- if (!KINDS.includes(kind) || listings.some((listing)=>listing.kind === kind)) continue;
87
+ // A records listing is one dataset's (AGL-3616): a site may list several.
88
+ const datasetId = kind === 'records' ? text(record['datasetId']) : '';
89
+ const fields = kind === 'records' ? listingFieldsOf(record['fields']) : [];
90
+ // Its fields may be left for the page to read off the site's inventory.
91
+ if (kind === 'records' && !DATASET_ID.test(datasetId)) continue;
92
+ if (!KINDS.includes(kind) || listings.some((listing)=>listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))) {
93
+ continue;
94
+ }
90
95
  const href = text(record['href']);
91
96
  const action = (_record_emptyAction = record['emptyAction']) != null ? _record_emptyAction : {};
92
97
  const actionLabel = text(action['label']).slice(0, 40);
@@ -108,15 +113,19 @@ const text = (value)=>typeof value === 'string' ? value.trim() : '';
108
113
  ] : [];
109
114
  });
110
115
  listings.push(_extends({
111
- id: aiLayoutListingId(kind),
116
+ id: aiLayoutListingId(kind, datasetId),
112
117
  kind,
113
- name: text(record['name']) || (kind === 'products' ? 'the shop' : kind === 'posts' ? 'the blog' : 'the music'),
114
- // A tracks listing names no records: no job sources a recording (AGL-3716).
115
- records: kind === 'tracks' ? [] : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12)
118
+ name: text(record['name']) || LISTING_NAMES[kind],
119
+ // A tracks listing names no records: no job sources a recording (AGL-3716);
120
+ // nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).
121
+ records: kind !== 'products' && kind !== 'posts' && kind !== 'records' ? [] : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12)
116
122
  }, SITE_PATH.test(href) ? {
117
123
  href
118
124
  } : {}, kind === 'posts' ? {
119
125
  collectionSlug
126
+ } : {}, kind === 'records' ? {
127
+ datasetId,
128
+ fields
120
129
  } : {}, kind === 'products' && record['cart'] === false ? {
121
130
  cart: false
122
131
  } : {}, kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? {
@@ -158,6 +167,29 @@ const POST_SECTION = /\b(blog|posts?|articles?|writing|journal|stories|essays?|l
158
167
  const byItems = most > 0 ? sections.findIndex((section)=>section.items === most) : -1;
159
168
  return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0;
160
169
  }
170
+ /**
171
+ * The section of a store's home its bestsellers fill (AGL-3676): the first
172
+ * after the hero whose name says them ("Bestsellers", "Featured candles", "New
173
+ * arrivals"), never one about the store's collections, its reviews or its
174
+ * sign-up; else the first after the hero naming products at all; else the
175
+ * first after the hero planned with items that is none of those. The
176
+ * beta.237 Willow Wick home would otherwise have listed products in its
177
+ * "Shop by collection" tiles.
178
+ */ function aiLayoutFeaturedSection(screen) {
179
+ // A section about the collections that does not also name the bestsellers.
180
+ const other = (name)=>aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name) || aiStorefrontSays('reviews', name) || aiStorefrontSays('signup', name);
181
+ const first = (test)=>screen.sections.findIndex((section, index)=>index > 0 && test(section.name, section.items));
182
+ for (const test of [
183
+ (name)=>aiStorefrontSays('featured', name) && !other(name),
184
+ (name)=>PRODUCT_SECTION.test(name) && !other(name),
185
+ (name, items)=>items > 0 && !other(name),
186
+ (name)=>PRODUCT_SECTION.test(name)
187
+ ]){
188
+ const found = first(test);
189
+ if (found !== -1) return found;
190
+ }
191
+ return -1;
192
+ }
161
193
  /** A section of a shop page that only opens or closes it: "Shop intro heading", "Gift help call to action". */ const SHOP_FRAMING = /\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\b/i;
162
194
  /** Whether a planned page is the store's own Shop page: its address or its name says it sells. */ export function aiLayoutIsShopPage(screen) {
163
195
  return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`);
@@ -211,11 +243,25 @@ const isHome = (slug)=>firstSegment(slug) === '';
211
243
  }
212
244
  return placements;
213
245
  }
246
+ if (kind === 'reviews' || kind === 'signup') {
247
+ // A store's own reviews, on its home, in the section named for them; its
248
+ // newsletter sign-up in any page's section named for it that places no
249
+ // form of the plan's (AGL-3676). Neither is ever a page's opening.
250
+ for (const screen of screens){
251
+ if (kind === 'reviews' && !isHome(screen.slug)) continue;
252
+ place(screen, afterHero(screen, (name)=>aiStorefrontSays(kind, name)), kind === 'reviews' ? 'featured' : 'index');
253
+ }
254
+ return kind === 'signup' ? placements.filter((placement)=>{
255
+ var _ref;
256
+ var _screen_sections_placement_section;
257
+ const screen = screens.find((entry)=>entry.id === placement.screenId);
258
+ return !((_ref = screen == null ? void 0 : (_screen_sections_placement_section = screen.sections[placement.section]) == null ? void 0 : _screen_sections_placement_section.uses) != null ? _ref : []).some((use)=>/form/i.test(use));
259
+ }) : placements;
260
+ }
214
261
  if (kind === 'products') {
215
262
  for (const screen of screens){
216
263
  if (isHome(screen.slug)) {
217
- const named = afterHero(screen, (name)=>PRODUCT_SECTION.test(name));
218
- place(screen, named !== -1 ? named : afterHero(screen, (_name, items)=>items > 0), 'featured');
264
+ place(screen, aiLayoutFeaturedSection(screen), 'featured');
219
265
  continue;
220
266
  }
221
267
  if (!aiLayoutIsShopPage(screen)) continue;
@@ -238,5 +284,27 @@ const isHome = (slug)=>firstSegment(slug) === '';
238
284
  }
239
285
  return placements;
240
286
  }
287
+ /**
288
+ * The sections that list one of the site's datasets (AGL-3616): every section
289
+ * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,
290
+ * or its id once built. The home page features a few of its records; any
291
+ * other page lists them all. Chosen from the plan, never asked of the model.
292
+ */ export function aiLayoutRecordsPlacements(dataset, screens) {
293
+ const named = `new:${dataset.name}`.trim().toLowerCase();
294
+ const placements = [];
295
+ for (const screen of screens){
296
+ if (typeof screen.id !== 'string' || !screen.id) continue;
297
+ screen.sections.forEach((section, index)=>{
298
+ var _section_uses;
299
+ const lists = ((_section_uses = section.uses) != null ? _section_uses : []).some((ref)=>ref === dataset.id || ref.trim().toLowerCase() === named);
300
+ if (lists) placements.push({
301
+ screenId: screen.id,
302
+ section: index,
303
+ role: isHome(screen.slug) ? 'featured' : 'index'
304
+ });
305
+ });
306
+ }
307
+ return placements;
308
+ }
241
309
 
242
310
  //# sourceMappingURL=ai-layout-listings.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.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\n/**\n * A SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).\n *\n * A guided start writes a store's first products and a blog's first posts,\n * then built the pages around them in the layout language — and the pages\n * showed neither. \"Featured candles\" and \"The candle range\" on the live\n * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a\n * line each: no photo, no price, no link, nothing a shopper could open; the\n * Clay Notes blog's \"Featured writing\" was three cards that linked to no\n * post. The real records were on the site, one click away from nowhere.\n *\n * A LISTING is one kind of record a site keeps — its catalog, its blog —\n * placed by code in the sections whose plan says they show it, through the\n * element the plugin that keeps the records renders them with, bound to the\n * records themselves rather than to words about them. It is the form binding\n * one step on (a section whose plan `uses` a form places that form by its\n * id, whatever the answer remembered): the model writes the section's\n * heading and words, the platform places the records.\n *\n * - `products` — the commerce plugin's Product grid (`product-grid`), over\n * the site's whole active catalog: a photo, the name, the price (or\n * \"Price coming soon\" where the owner cleared it), each card linking its\n * product's page.\n * - `posts` — the content plugin's Collection Entries (`collectionEntries`)\n * over the blog's collection, its card a cover, the date and byline, the\n * title and the excerpt, linking each post.\n * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n *\n * The elements are named by their persisted ids, as the form binding names\n * `form`: a plugin never imports another's internals, and\n * `ai-layout-listings.spec.ts` holds the ids and props named here to the\n * source of the plugins that declare them.\n *\n * Model only: shapes and the inputs reader; the compiler draws them.\n */\n\nexport type AiLayoutListingKind = 'products' | 'posts' | 'tracks'\n\n/** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */\nexport type AiLayoutListingRole = 'featured' | 'index'\n\n/** One section of one page that places a listing. */\nexport interface AiLayoutListingPlacement {\n /** The page, by its plan id. */\n screenId: string\n /** The section, by plan index. */\n section: number\n role: AiLayoutListingRole\n}\n\n/** A kind of record the site keeps, and where its pages place it. */\nexport interface AiLayoutListing {\n /** `listing:products`, `listing:posts`: what a section's placement names. */\n id: string\n kind: AiLayoutListingKind\n /** What a visitor would call the whole list: \"the shop\", \"the blog\". */\n name: string\n /** The records it shows today, by name, so a page's words can name them. */\n records: string[]\n /** Where the whole list lives, as a path on the site: the blog's `/blog`. */\n href?: string\n /** The content collection a posts listing repeats, by its slug. */\n collectionSlug?: string\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\n placements: AiLayoutListingPlacement[]\n}\n\n/** The unit input a site's listings travel in, to its layout and its pages. */\nexport const AI_LAYOUT_LISTINGS_INPUT = 'siteListings'\n\n/** The id a listing of a kind is placed by. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind): string {\n return `listing:${kind}`\n}\n\n/** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */\nexport const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'product-grid',\n posts: 'collectionEntries',\n tracks: 'musicPlayer',\n}\n\n/** The header's cart button: the commerce plugin's Cart in its `button` variant. */\nexport const AI_LAYOUT_CART_ELEMENT = 'cart'\n\n/** The most records a featured band shows; a page of its own shows them all. */\nexport const AI_LAYOUT_FEATURED_RECORDS = { products: 4, posts: 3, tracks: 0 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A site's listings as its unit inputs carry them; nothing that does not read is kept. */\nexport function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[] {\n const raw = inputs?.[AI_LAYOUT_LISTINGS_INPUT]\n if (!Array.isArray(raw)) return []\n const listings: AiLayoutListing[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n const kind = record['kind'] as AiLayoutListingKind\n if (!KINDS.includes(kind) || listings.some((listing) => listing.kind === kind)) continue\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['href'])\n const collectionSlug = text(record['collectionSlug'])\n // A posts listing with no collection to repeat shows nothing.\n if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue\n const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value) => {\n const placement = (value ?? {}) as Record<string, unknown>\n const screenId = text(placement['screenId'])\n const section = placement['section']\n const role = placement['role'] as AiLayoutListingRole\n return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role)\n ? [{ screenId, section, role }]\n : []\n })\n listings.push({\n id: aiLayoutListingId(kind),\n kind,\n name: text(record['name']) || (kind === 'products' ? 'the shop' : kind === 'posts' ? 'the blog' : 'the music'),\n // A tracks listing names no records: no job sources a recording (AGL-3716).\n records:\n kind === 'tracks'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number; uses?: readonly string[] }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n const named = afterHero(screen, (name) => PRODUCT_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n"],"names":["AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","SITE_PATH","aiLayoutListingsOf","inputs","raw","Array","isArray","listings","entry","record","includes","some","listing","href","action","actionLabel","slice","actionHref","collectionSlug","test","placements","flatMap","placement","screenId","section","role","Number","isInteger","push","id","name","records","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutIsShopPage","screen","isHome","slug","firstSegment","title","replace","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","_name"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC;AA2CD,6EAA6E,GAC7E,OAAO,MAAMA,2BAA2B,eAAc;AAEtD,6CAA6C,GAC7C,OAAO,SAASC,kBAAkBC,IAAyB;IACzD,OAAO,CAAC,QAAQ,EAAEA,MAAM;AAC1B;AAEA,oGAAoG,GACpG,OAAO,MAAMC,6BAA4E;IACvFC,UAAU;IACVC,OAAO;IACPC,QAAQ;AACV,EAAC;AAED,kFAAkF,GAClF,OAAO,MAAMC,yBAAyB,OAAM;AAE5C,8EAA8E,GAC9E,OAAO,MAAMC,6BAA6B;IAAEJ,UAAU;IAAGC,OAAO;IAAGC,QAAQ;AAAE,EAAU;AAEvF,MAAMG,kBAAkD;IAAC;IAAY;CAAQ;AAC7E,MAAMC,QAAwC;IAAC;IAAY;IAAS;CAAS;AAE7E,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAErF,uDAAuD,GACvD,MAAMC,YAAY;AAElB,yFAAyF,GACzF,OAAO,SAASC,mBAAmBC,MAA4D;IAC7F,MAAMC,MAAMD,0BAAAA,MAAQ,CAAChB,yBAAyB;IAC9C,IAAI,CAACkB,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,MAAMG,WAA8B,EAAE;IACtC,KAAK,MAAMC,SAASJ,IAAK;YAMPK;QALhB,IAAI,CAACD,SAAS,OAAOA,UAAU,UAAU;QACzC,MAAMC,SAASD;QACf,MAAMnB,OAAOoB,MAAM,CAAC,OAAO;QAC3B,IAAI,CAACZ,MAAMa,QAAQ,CAACrB,SAASkB,SAASI,IAAI,CAAC,CAACC,UAAYA,QAAQvB,IAAI,KAAKA,OAAO;QAChF,MAAMwB,OAAOf,KAAKW,MAAM,CAAC,OAAO;QAChC,MAAMK,UAAUL,sBAAAA,MAAM,CAAC,cAAc,YAArBA,sBAAyB,CAAC;QAC1C,MAAMM,cAAcjB,KAAKgB,MAAM,CAAC,QAAQ,EAAEE,KAAK,CAAC,GAAG;QACnD,MAAMC,aAAanB,KAAKgB,MAAM,CAAC,OAAO;QACtC,MAAMI,iBAAiBpB,KAAKW,MAAM,CAAC,iBAAiB;QACpD,8DAA8D;QAC9D,IAAIpB,SAAS,WAAW,CAAC,qBAAqB8B,IAAI,CAACD,iBAAiB;QACpE,MAAME,aAAa,AAACf,CAAAA,MAAMC,OAAO,CAACG,MAAM,CAAC,aAAa,IAAIA,MAAM,CAAC,aAAa,GAAG,EAAE,AAAD,EAAGY,OAAO,CAAC,CAACtB;YAC5F,MAAMuB,YAAavB,gBAAAA,QAAS,CAAC;YAC7B,MAAMwB,WAAWzB,KAAKwB,SAAS,CAAC,WAAW;YAC3C,MAAME,UAAUF,SAAS,CAAC,UAAU;YACpC,MAAMG,OAAOH,SAAS,CAAC,OAAO;YAC9B,OAAOC,YAAY,OAAOC,YAAY,YAAYE,OAAOC,SAAS,CAACH,YAAYA,WAAW,KAAK5B,gBAAgBc,QAAQ,CAACe,QACpH;gBAAC;oBAAEF;oBAAUC;oBAASC;gBAAK;aAAE,GAC7B,EAAE;QACR;QACAlB,SAASqB,IAAI,CAAC;YACZC,IAAIzC,kBAAkBC;YACtBA;YACAyC,MAAMhC,KAAKW,MAAM,CAAC,OAAO,KAAMpB,CAAAA,SAAS,aAAa,aAAaA,SAAS,UAAU,aAAa,WAAU;YAC5G,4EAA4E;YAC5E0C,SACE1C,SAAS,WACL,EAAE,GACF,AAACgB,CAAAA,MAAMC,OAAO,CAACG,MAAM,CAAC,UAAU,IAAIA,MAAM,CAAC,UAAU,GAAG,EAAE,AAAD,EAAGuB,GAAG,CAAClC,MAAMmC,MAAM,CAACC,SAASlB,KAAK,CAAC,GAAG;WACjGf,UAAUkB,IAAI,CAACN,QAAQ;YAAEA;QAAK,IAAI,CAAC,GACnCxB,SAAS,UAAU;YAAE6B;QAAe,IAAI,CAAC,GACzC7B,SAAS,cAAcoB,MAAM,CAAC,OAAO,KAAK,QAAQ;YAAE0B,MAAM;QAAM,IAAI,CAAC,GACrE9C,SAAS,cAAc0B,eAAed,UAAUkB,IAAI,CAACF,cAAc;YAAEmB,aAAa;gBAAEC,OAAOtB;gBAAaF,MAAMI;YAAW;QAAE,IAAI,CAAC;YACpIG;;IAEJ;IACA,OAAOb;AACT;AAEA,4FAA4F,GAC5F,OAAO,SAAS+B,kBACd/B,QAAoC,EACpCgB,QAAmC,EACnCC,OAAe;IAEf,IAAI,CAACD,UAAU,OAAO;IACtB,KAAK,MAAMX,WAAWL,SAAU;QAC9B,MAAMe,YAAYV,QAAQQ,UAAU,CAACmB,IAAI,CAAC,CAAC/B,QAAUA,MAAMe,QAAQ,KAAKA,YAAYf,MAAMgB,OAAO,KAAKA;QACtG,IAAIF,WAAW,OAAO;YAAEV;YAASa,MAAMH,UAAUG,IAAI;QAAC;IACxD;IACA,OAAO;AACT;AAYA,MAAMe,eAAe;AACrB,MAAMC,kBACJ;AACF,MAAMC,eAAe;AACrB,oEAAoE,GACpE,MAAMC,aAAa;AAEnB;;;;;;CAMC,GACD,OAAO,SAASC,wBAAwBC,QAAwD;IAC9F,MAAMC,QAAQD,SAASE,SAAS,CAAC,CAACvB,SAASwB,QAAUA,QAAQ,KAAKP,gBAAgBtB,IAAI,CAACK,QAAQM,IAAI,KAAK,CAACmB,aAAa9B,IAAI,CAACK,QAAQM,IAAI;IACvI,IAAIgB,UAAU,CAAC,GAAG,OAAOA;IACzB,MAAMI,OAAOC,KAAKC,GAAG,CAAC,MAAMP,SAASb,GAAG,CAAC,CAACR,UAAYA,QAAQ6B,KAAK;IACnE,MAAMC,UAAUJ,OAAO,IAAIL,SAASE,SAAS,CAAC,CAACvB,UAAYA,QAAQ6B,KAAK,KAAKH,QAAQ,CAAC;IACtF,OAAOI,YAAY,CAAC,IAAIA,UAAUT,SAASU,MAAM,GAAG,IAAI,IAAI;AAC9D;AAEA,6GAA6G,GAC7G,MAAMN,eAAe;AAErB,gGAAgG,GAChG,OAAO,SAASO,mBAAmBC,MAAuC;IACxE,OAAO,CAACC,OAAOD,OAAOE,IAAI,KAAKnB,aAAarB,IAAI,CAAC,GAAGyC,aAAaH,OAAOE,IAAI,EAAE,CAAC,EAAEF,OAAOI,KAAK,EAAE;AACjG;AAEA,MAAMD,eAAe,CAACD,OAAiBA,KAAK3D,IAAI,GAAG8D,OAAO,CAAC,QAAQ,IAAIC,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,WAAW;AAChG,MAAMN,SAAS,CAACC,OAAiBC,aAAaD,UAAU;AAExD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASM,0BACd5E,IAAyB,EACzB6E,OAAyC;IAEzC,MAAM9C,aAAyC,EAAE;IACjD,MAAM+C,QAAQ,CAACV,QAA+BjC,SAAiBC;QAC7D,IAAI,OAAOgC,OAAO5B,EAAE,KAAK,YAAY4B,OAAO5B,EAAE,IAAIL,WAAW,GAAGJ,WAAWQ,IAAI,CAAC;YAAEL,UAAUkC,OAAO5B,EAAE;YAAEL;YAASC;QAAK;IACvH;IACA,MAAM2C,YAAY,CAACX,QAA+BtC,OAChDsC,OAAOZ,QAAQ,CAACE,SAAS,CAAC,CAACvB,SAASwB,QAAUA,QAAQ,KAAK7B,KAAKK,QAAQM,IAAI,EAAEN,QAAQ6B,KAAK;IAC7F,IAAIhE,SAAS,UAAU;QACrB,2EAA2E;QAC3E,0EAA0E;QAC1E,yEAAyE;QACzE,+BAA+B;QAC/B,KAAK,MAAMoE,UAAUS,QAAS;YAC5B,IAAIR,OAAOD,OAAOE,IAAI,GAAG;gBACvBQ,MAAMV,QAAQW,UAAUX,QAAQ,CAAC3B,OAASa,WAAWxB,IAAI,CAACW,QAAQ;gBAClE;YACF;YACA,IAAI,CAACa,WAAWxB,IAAI,CAAC,GAAGyC,aAAaH,OAAOE,IAAI,EAAE,CAAC,EAAEF,OAAOI,KAAK,EAAE,GAAG;YACtE,MAAMf,QAAQsB,UAAUX,QAAQ,CAAC3B,OAASa,WAAWxB,IAAI,CAACW;YAC1DqC,MAAMV,QAAQX,UAAU,CAAC,IAAIA,QAAQW,OAAOZ,QAAQ,CAACU,MAAM,GAAG,IAAI,IAAI,GAAG;QAC3E;QACA,IAAI,CAACnC,WAAWmC,MAAM,EAAE;YACtB,MAAMc,OAAOH,QAAQ3B,IAAI,CAAC,CAACkB,SAAWC,OAAOD,OAAOE,IAAI;YACxD,IAAIU,MAAMF,MAAME,MAAMA,KAAKxB,QAAQ,CAACU,MAAM,GAAG,IAAI,IAAI,GAAG;QAC1D;QACA,OAAOnC;IACT;IACA,IAAI/B,SAAS,YAAY;QACvB,KAAK,MAAMoE,UAAUS,QAAS;YAC5B,IAAIR,OAAOD,OAAOE,IAAI,GAAG;gBACvB,MAAMb,QAAQsB,UAAUX,QAAQ,CAAC3B,OAASW,gBAAgBtB,IAAI,CAACW;gBAC/DqC,MAAMV,QAAQX,UAAU,CAAC,IAAIA,QAAQsB,UAAUX,QAAQ,CAACa,OAAOjB,QAAUA,QAAQ,IAAI;gBACrF;YACF;YACA,IAAI,CAACG,mBAAmBC,SAAS;YACjCU,MAAMV,QAAQb,wBAAwBa,OAAOZ,QAAQ,GAAG;QAC1D;QACA,OAAOzB;IACT;IACA,KAAK,MAAMqC,UAAUS,QAAS;QAC5B,IAAIR,OAAOD,OAAOE,IAAI,GAAG;YACvB,oEAAoE;YACpE,wEAAwE;YACxE,uEAAuE;YACvE,gEAAgE;YAChE,MAAMb,QAAQsB,UAAUX,QAAQ,CAAC3B,MAAMuB,QAAUA,QAAQ,KAAKX,aAAavB,IAAI,CAACW;YAChFqC,MAAMV,QAAQX,UAAU,CAAC,IAAIA,QAAQsB,UAAUX,QAAQ,CAACa,OAAOjB,QAAUA,QAAQ,IAAI;YACrF;QACF;QACA,IAAI,CAACX,aAAavB,IAAI,CAACsC,OAAOI,KAAK,GAAG;QACtCM,MAAMV,QAAQW,UAAUX,QAAQ,CAAC3B,OAASY,aAAavB,IAAI,CAACW,QAAQ;IACtE;IACA,OAAOV;AACT"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-listings.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 { aiStorefrontSays } from './ai-layout-storefront'\n\n/**\n * A SITE'S OWN RECORDS, PLACED BY THE PLATFORM (AGL-3676).\n *\n * A guided start writes a store's first products and a blog's first posts,\n * then built the pages around them in the layout language — and the pages\n * showed neither. \"Featured candles\" and \"The candle range\" on the live\n * Ember & Wick start (2026-10-08) were cards the model wrote, a name and a\n * line each: no photo, no price, no link, nothing a shopper could open; the\n * Clay Notes blog's \"Featured writing\" was three cards that linked to no\n * post. The real records were on the site, one click away from nowhere.\n *\n * A LISTING is one kind of record a site keeps — its catalog, its blog —\n * placed by code in the sections whose plan says they show it, through the\n * element the plugin that keeps the records renders them with, bound to the\n * records themselves rather than to words about them. It is the form binding\n * one step on (a section whose plan `uses` a form places that form by its\n * id, whatever the answer remembered): the model writes the section's\n * heading and words, the platform places the records.\n *\n * - `products` — the commerce plugin's Product grid (`product-grid`), over\n * the site's whole active catalog: a photo, the name, the price (or\n * \"Price coming soon\" where the owner cleared it), each card linking its\n * product's page.\n * - `posts` — the content plugin's Collection Entries (`collectionEntries`)\n * over the blog's collection, its card a cover, the date and byline, the\n * title and the excerpt, linking each post.\n * - `tracks` — the music plugin's Music player (`musicPlayer`, AGL-3716) on a\n * music site, placed EMPTY: its \"add your tracks\" state, for the owner to\n * fill from their own media library. No job ever sources a recording, so\n * a tracks listing has no records and the player no source.\n * - `reviews` — a selling store's own customer reviews, through the commerce\n * plugin's Product reviews in its store-wide scope (`product-reviews`,\n * AGL-3676): approved reviews across the catalog with their star ratings.\n * A store that opens has none, and the element then shows nothing at all\n * on the published page, so no review is ever written for a shopper who\n * did not leave one (the FTC's fake-review rule; rule 14's \"never invent\n * a customer\").\n * - `signup` — a selling store's newsletter sign-up, through the commerce\n * plugin's Newsletter signup (`newsletter-signup`, AGL-3676): a consented\n * email field into the store's contacts, where a section says newsletter.\n * - `records` — one of the site's datasets (AGL-3616): a menu, a team, its\n * services, built by the site start and kept by the data plugin. Drawn as\n * a Box that repeats over the dataset (`repeatDataset`, the platform's own\n * repeat), its card binding the record's fields as `{{item.<field>}}`, in\n * the sections whose plan names the dataset in its `uses`. A site may list\n * several, one listing a dataset.\n *\n * The elements are named by their persisted ids, as the form binding names\n * `form`: a plugin never imports another's internals, and\n * `ai-layout-listings.spec.ts` holds the ids and props named here to the\n * source of the plugins that declare them.\n *\n * Model only: shapes and the inputs reader; the compiler draws them.\n */\n\nexport type AiLayoutListingKind = 'products' | 'posts' | 'tracks' | 'reviews' | 'signup' | 'records'\n\n/** How a section shows a listing: a few of its records on a page about something else, or all of them on its own page. */\nexport type AiLayoutListingRole = 'featured' | 'index'\n\n/** One section of one page that places a listing. */\nexport interface AiLayoutListingPlacement {\n /** The page, by its plan id. */\n screenId: string\n /** The section, by plan index. */\n section: number\n role: AiLayoutListingRole\n}\n\n/** One field of a dataset a `records` listing shows, as its card binds it. */\nexport interface AiLayoutListingField {\n /** The field's id, which `{{item.<id>}}` reads. */\n id: string\n name: string\n /** The data plugin's stored type: `text`, `float`, `int32`, `bool`, `sorted`. */\n type: string\n}\n\n/** A kind of record the site keeps, and where its pages place it. */\nexport interface AiLayoutListing {\n /** `listing:products`, `listing:posts`: what a section's placement names. */\n id: string\n kind: AiLayoutListingKind\n /** What a visitor would call the whole list: \"the shop\", \"the blog\". */\n name: string\n /** The records it shows today, by name, so a page's words can name them. */\n records: string[]\n /** Where the whole list lives, as a path on the site: the blog's `/blog`. */\n href?: string\n /** The content collection a posts listing repeats, by its slug. */\n collectionSlug?: string\n /** The dataset a records listing repeats over, by id (AGL-3616). */\n datasetId?: string\n /** The dataset's fields, in its order, which the card binds. */\n fields?: AiLayoutListingField[]\n /**\n * Where a visitor goes from a store with nothing listed yet (AGL-3676): the\n * site's contact page, by its path. The empty state names it as its one\n * action, so a shop that opens empty still answers a visitor.\n */\n emptyAction?: { label: string; href: string }\n /**\n * `false` for a store that lists its catalog but cannot sell yet (its first\n * products were skipped or failed, AGL-3676): the header carries no cart.\n */\n cart?: boolean\n placements: AiLayoutListingPlacement[]\n}\n\n/** The unit input a site's listings travel in, to its layout and its pages. */\nexport const AI_LAYOUT_LISTINGS_INPUT = 'siteListings'\n\n/** The id a listing of a kind is placed by; a records listing is one dataset's. */\nexport function aiLayoutListingId(kind: AiLayoutListingKind, datasetId?: string): string {\n return kind === 'records' && datasetId ? `listing:records:${datasetId}` : `listing:${kind}`\n}\n\n/** The persisted element a listing is drawn with, by kind (held to its plugin's source by a spec). */\nexport const AI_LAYOUT_LISTING_ELEMENTS: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'product-grid',\n posts: 'collectionEntries',\n tracks: 'musicPlayer',\n reviews: 'product-reviews',\n signup: 'newsletter-signup',\n // The platform's own Box, repeating its card over the dataset.\n records: 'muiBox',\n}\n\n/** The header's cart button: the commerce plugin's Cart in its `button` variant. */\nexport const AI_LAYOUT_CART_ELEMENT = 'cart'\n\n/** The most records a featured band shows; a page of its own shows them all. */\nexport const AI_LAYOUT_FEATURED_RECORDS = { products: 4, posts: 3, tracks: 0, reviews: 3, signup: 0, records: 3 } as const\n\nconst PLACEMENT_ROLES: readonly AiLayoutListingRole[] = ['featured', 'index']\nconst KINDS: readonly AiLayoutListingKind[] = ['products', 'posts', 'tracks', 'reviews', 'signup', 'records']\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What a visitor calls each kind's whole list, where a unit's input names none. */\nconst LISTING_NAMES: Readonly<Record<AiLayoutListingKind, string>> = {\n products: 'the shop',\n posts: 'the blog',\n tracks: 'the music',\n reviews: 'the store',\n signup: 'the newsletter',\n records: 'the list',\n}\n\n/** A path on the site, as a listing's links are kept. */\nconst SITE_PATH = /^\\/[a-z0-9-]+(?:\\/[a-z0-9-]+)*$/\n\n/** A dataset id and a field id, as the data plugin mints them. */\nconst DATASET_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst FIELD_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/\n\n/** A records listing's fields as its input carries them; nothing that does not read is kept. */\nfunction listingFieldsOf(raw: unknown): AiLayoutListingField[] {\n if (!Array.isArray(raw)) return []\n return raw\n .flatMap((value) => {\n const field = (value ?? {}) as Record<string, unknown>\n const id = text(field['id'])\n return FIELD_ID.test(id) ? [{ id, name: text(field['name']).slice(0, 60) || id, type: text(field['type']) || 'text' }] : []\n })\n .slice(0, 16)\n}\n\n/** A site's listings as its unit inputs carry them; nothing that does not read is kept. */\nexport function aiLayoutListingsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutListing[] {\n const raw = inputs?.[AI_LAYOUT_LISTINGS_INPUT]\n if (!Array.isArray(raw)) return []\n const listings: AiLayoutListing[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n const kind = record['kind'] as AiLayoutListingKind\n // A records listing is one dataset's (AGL-3616): a site may list several.\n const datasetId = kind === 'records' ? text(record['datasetId']) : ''\n const fields = kind === 'records' ? listingFieldsOf(record['fields']) : []\n // Its fields may be left for the page to read off the site's inventory.\n if (kind === 'records' && !DATASET_ID.test(datasetId)) continue\n if (\n !KINDS.includes(kind) ||\n listings.some((listing) => listing.kind === kind && (kind !== 'records' || listing.datasetId === datasetId))\n ) {\n continue\n }\n const href = text(record['href'])\n const action = (record['emptyAction'] ?? {}) as Record<string, unknown>\n const actionLabel = text(action['label']).slice(0, 40)\n const actionHref = text(action['href'])\n const collectionSlug = text(record['collectionSlug'])\n // A posts listing with no collection to repeat shows nothing.\n if (kind === 'posts' && !/^[a-z0-9-]{1,100}$/.test(collectionSlug)) continue\n const placements = (Array.isArray(record['placements']) ? record['placements'] : []).flatMap((value) => {\n const placement = (value ?? {}) as Record<string, unknown>\n const screenId = text(placement['screenId'])\n const section = placement['section']\n const role = placement['role'] as AiLayoutListingRole\n return screenId && typeof section === 'number' && Number.isInteger(section) && section >= 0 && PLACEMENT_ROLES.includes(role)\n ? [{ screenId, section, role }]\n : []\n })\n listings.push({\n id: aiLayoutListingId(kind, datasetId),\n kind,\n name: text(record['name']) || LISTING_NAMES[kind],\n // A tracks listing names no records: no job sources a recording (AGL-3716);\n // nor do a store's reviews and its sign-up, which shoppers fill (AGL-3676).\n records:\n kind !== 'products' && kind !== 'posts' && kind !== 'records'\n ? []\n : (Array.isArray(record['records']) ? record['records'] : []).map(text).filter(Boolean).slice(0, 12),\n ...(SITE_PATH.test(href) ? { href } : {}),\n ...(kind === 'posts' ? { collectionSlug } : {}),\n ...(kind === 'records' ? { datasetId, fields } : {}),\n ...(kind === 'products' && record['cart'] === false ? { cart: false } : {}),\n ...(kind === 'products' && actionLabel && SITE_PATH.test(actionHref) ? { emptyAction: { label: actionLabel, href: actionHref } } : {}),\n placements,\n })\n }\n return listings\n}\n\n/** The listing a section of a page places, and how; `null` for a section that places none. */\nexport function aiLayoutListingAt(\n listings: readonly AiLayoutListing[],\n screenId: string | null | undefined,\n section: number,\n): { listing: AiLayoutListing; role: AiLayoutListingRole } | null {\n if (!screenId) return null\n for (const listing of listings) {\n const placement = listing.placements.find((entry) => entry.screenId === screenId && entry.section === section)\n if (placement) return { listing, role: placement.role }\n }\n return null\n}\n\n// ── Where a site's pages place them ────────────────────────────────────\n\n/** The plan of a page, as a placement is chosen from it. */\nexport interface AiLayoutListingScreen {\n id?: string | null\n title: string\n slug: string\n sections: ReadonlyArray<{ name: string; items: number; uses?: readonly string[] }>\n}\n\nconst PRODUCT_PAGE = /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|browse)\\b/i\nconst PRODUCT_SECTION =\n /\\b(shop|store|products?|catalog(?:ue)?|collections?|range|bestsellers?|best[- ]sellers?|new arrivals|arrivals|featured|favou?rites|signature)\\b/i\nconst POST_SECTION = /\\b(blog|posts?|articles?|writing|journal|stories|essays?|latest|recent|news|featured)\\b/i\n/** A music site's page or section about its recordings (AGL-3716). */\nconst MUSIC_PAGE = /\\b(music|listen|tracks?|songs?|releases?|discography|albums?|eps?|singles?|sounds?)\\b/i\n\n/**\n * The section of a shop's own page the whole catalog fills (AGL-3676): the\n * first after its opening whose name says products (\"Product grid\", \"The\n * range\"), else the section planned with the most items, else its second.\n * The live Hearth & Wick start (2026-10-09) planned \"Product range image\n * cards\" — six cards naming kinds of candle, with no product, price or cart.\n */\nexport function aiLayoutShopGridSection(sections: ReadonlyArray<{ name: string; items: number }>): number {\n const named = sections.findIndex((section, index) => index > 0 && PRODUCT_SECTION.test(section.name) && !SHOP_FRAMING.test(section.name))\n if (named !== -1) return named\n const most = Math.max(0, ...sections.map((section) => section.items))\n const byItems = most > 0 ? sections.findIndex((section) => section.items === most) : -1\n return byItems !== -1 ? byItems : sections.length > 1 ? 1 : 0\n}\n\n/**\n * The section of a store's home its bestsellers fill (AGL-3676): the first\n * after the hero whose name says them (\"Bestsellers\", \"Featured candles\", \"New\n * arrivals\"), never one about the store's collections, its reviews or its\n * sign-up; else the first after the hero naming products at all; else the\n * first after the hero planned with items that is none of those. The\n * beta.237 Willow Wick home would otherwise have listed products in its\n * \"Shop by collection\" tiles.\n */\nfunction aiLayoutFeaturedSection(screen: AiLayoutListingScreen): number {\n // A section about the collections that does not also name the bestsellers.\n const other = (name: string) =>\n (aiStorefrontSays('collections', name) && !aiStorefrontSays('featured', name)) ||\n aiStorefrontSays('reviews', name) ||\n aiStorefrontSays('signup', name)\n const first = (test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n for (const test of [\n (name: string) => aiStorefrontSays('featured', name) && !other(name),\n (name: string) => PRODUCT_SECTION.test(name) && !other(name),\n (name: string, items: number) => items > 0 && !other(name),\n (name: string) => PRODUCT_SECTION.test(name),\n ]) {\n const found = first(test)\n if (found !== -1) return found\n }\n return -1\n}\n\n/** A section of a shop page that only opens or closes it: \"Shop intro heading\", \"Gift help call to action\". */\nconst SHOP_FRAMING = /\\b(hero|intro(?:duction)?|heading|banner|cta|call to action|contact)\\b/i\n\n/** Whether a planned page is the store's own Shop page: its address or its name says it sells. */\nexport function aiLayoutIsShopPage(screen: { slug: string; title: string }): boolean {\n return !isHome(screen.slug) && PRODUCT_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst isHome = (slug: string) => firstSegment(slug) === ''\n\n/**\n * The sections of a site's pages that show a listing, chosen by code from the\n * plan (AGL-3676), never asked of the model:\n *\n * - PRODUCTS. The shop's own page — the page whose address or name says it\n * sells (`/shop`, \"Shop\", \"Products\") — shows the whole catalog in the\n * section the plan gave the most items, else its second; and the home\n * page features a few in its first section after the hero whose name\n * says products (\"Featured candles\", \"Bestsellers\"), else in the first\n * section after the hero planned with items.\n * - POSTS. The home features the latest in its first section of items after\n * the hero whose name says writing (\"Featured writing\", \"Latest posts\"),\n * else its first section of items after the hero; and any other page\n * whose name says it is about the writing does the same. The blog's own\n * index is the platform's.\n *\n * A section placed once is placed for one listing.\n */\nexport function aiLayoutListingPlacements(\n kind: AiLayoutListingKind,\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const placements: AiLayoutListingPlacement[] = []\n const place = (screen: AiLayoutListingScreen, section: number, role: AiLayoutListingRole) => {\n if (typeof screen.id === 'string' && screen.id && section >= 0) placements.push({ screenId: screen.id, section, role })\n }\n const afterHero = (screen: AiLayoutListingScreen, test: (name: string, items: number) => boolean) =>\n screen.sections.findIndex((section, index) => index > 0 && test(section.name, section.items))\n if (kind === 'tracks') {\n // A music site's player (AGL-3716): on its Music page, in the section that\n // says it is about the recordings, else the one after the opening; on the\n // home, only in a section named for them. A site with neither gets it on\n // its home, after the opening.\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, afterHero(screen, (name) => MUSIC_PAGE.test(name)), 'featured')\n continue\n }\n if (!MUSIC_PAGE.test(`${firstSegment(screen.slug)} ${screen.title}`)) continue\n const named = afterHero(screen, (name) => MUSIC_PAGE.test(name))\n place(screen, named !== -1 ? named : screen.sections.length > 1 ? 1 : 0, 'index')\n }\n if (!placements.length) {\n const home = screens.find((screen) => isHome(screen.slug))\n if (home) place(home, home.sections.length > 1 ? 1 : 0, 'featured')\n }\n return placements\n }\n if (kind === 'reviews' || kind === 'signup') {\n // A store's own reviews, on its home, in the section named for them; its\n // newsletter sign-up in any page's section named for it that places no\n // form of the plan's (AGL-3676). Neither is ever a page's opening.\n for (const screen of screens) {\n if (kind === 'reviews' && !isHome(screen.slug)) continue\n place(\n screen,\n afterHero(\n screen,\n (name) => aiStorefrontSays(kind, name),\n ),\n kind === 'reviews' ? 'featured' : 'index',\n )\n }\n return kind === 'signup'\n ? placements.filter((placement) => {\n const screen = screens.find((entry) => entry.id === placement.screenId)\n return !(screen?.sections[placement.section]?.uses ?? []).some((use) => /form/i.test(use))\n })\n : placements\n }\n if (kind === 'products') {\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n place(screen, aiLayoutFeaturedSection(screen), 'featured')\n continue\n }\n if (!aiLayoutIsShopPage(screen)) continue\n place(screen, aiLayoutShopGridSection(screen.sections), 'index')\n }\n return placements\n }\n for (const screen of screens) {\n if (isHome(screen.slug)) {\n // On a blog's home the band that names the writing (\"Featured posts\n // cards\", \"Featured journeys\") lists it; failing one, the first band of\n // items after the hero. A live Clay Notes plan (2026-10-09) opened its\n // home with \"What you will find\" before \"Featured posts cards\".\n const named = afterHero(screen, (name, items) => items > 0 && POST_SECTION.test(name))\n place(screen, named !== -1 ? named : afterHero(screen, (_name, items) => items > 0), 'featured')\n continue\n }\n if (!POST_SECTION.test(screen.title)) continue\n place(screen, afterHero(screen, (name) => POST_SECTION.test(name)), 'featured')\n }\n return placements\n}\n\n/**\n * The sections that list one of the site's datasets (AGL-3616): every section\n * whose plan names it in its `uses` — `new:<its name>` as the plan wrote it,\n * or its id once built. The home page features a few of its records; any\n * other page lists them all. Chosen from the plan, never asked of the model.\n */\nexport function aiLayoutRecordsPlacements(\n dataset: { id: string; name: string },\n screens: readonly AiLayoutListingScreen[],\n): AiLayoutListingPlacement[] {\n const named = `new:${dataset.name}`.trim().toLowerCase()\n const placements: AiLayoutListingPlacement[] = []\n for (const screen of screens) {\n if (typeof screen.id !== 'string' || !screen.id) continue\n screen.sections.forEach((section, index) => {\n const lists = (section.uses ?? []).some((ref) => ref === dataset.id || ref.trim().toLowerCase() === named)\n if (lists) placements.push({ screenId: screen.id as string, section: index, role: isHome(screen.slug) ? 'featured' : 'index' })\n })\n }\n return placements\n}\n"],"names":["aiStorefrontSays","AI_LAYOUT_LISTINGS_INPUT","aiLayoutListingId","kind","datasetId","AI_LAYOUT_LISTING_ELEMENTS","products","posts","tracks","reviews","signup","records","AI_LAYOUT_CART_ELEMENT","AI_LAYOUT_FEATURED_RECORDS","PLACEMENT_ROLES","KINDS","text","value","trim","LISTING_NAMES","SITE_PATH","DATASET_ID","FIELD_ID","listingFieldsOf","raw","Array","isArray","flatMap","field","id","test","name","slice","type","aiLayoutListingsOf","inputs","listings","entry","record","fields","includes","some","listing","href","action","actionLabel","actionHref","collectionSlug","placements","placement","screenId","section","role","Number","isInteger","push","map","filter","Boolean","cart","emptyAction","label","aiLayoutListingAt","find","PRODUCT_PAGE","PRODUCT_SECTION","POST_SECTION","MUSIC_PAGE","aiLayoutShopGridSection","sections","named","findIndex","index","SHOP_FRAMING","most","Math","max","items","byItems","length","aiLayoutFeaturedSection","screen","other","first","found","aiLayoutIsShopPage","isHome","slug","firstSegment","title","replace","split","toLowerCase","aiLayoutListingPlacements","screens","place","afterHero","home","uses","use","_name","aiLayoutRecordsPlacements","dataset","forEach","lists","ref"],"mappings":"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"}
@@ -53,6 +53,14 @@ export interface AiLayoutPictureSlot {
53
53
  /** The plan index of the section it sits in, or -1. */
54
54
  sectionIndex: number;
55
55
  role: AiLayoutPictureRole;
56
+ /**
57
+ * A product's photo (AGL-3676): what it is of, most specific first — its
58
+ * name, then what its photo should show — searched for with the shop's
59
+ * category and held to it, so a wick trimmer is never a laptop on a desk.
60
+ */
61
+ product?: {
62
+ subjects: readonly string[];
63
+ };
56
64
  }
57
65
  /** A photo a slot is filled with. */
58
66
  export interface AiLayoutPicturePhoto {
@@ -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'\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. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\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\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\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 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. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\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 }\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. A slot given no photo is left as\n * 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 slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\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 one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\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, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * 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 },\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 const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\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","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","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","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","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"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'\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. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\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\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\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 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. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\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 }\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. A slot given no photo is left as\n * 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 slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n next[slot.imageId] = {\n ...node,\n props: {\n ...((node['props'] as Record<string, unknown> | undefined) ?? {}),\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n loading: slot.role === 'hero' ? 'eager' : 'lazy',\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 one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\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, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * 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 },\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 const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\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","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","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","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","width","height","aiLayoutPlacePictures","photos","next","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}