@aglyn/plugins-ai 1.0.0-beta.291 → 1.0.0-beta.293

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 (149) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-site-import-job.component.js +5 -4
  3. package/src/lib/components/ai-site-import-job.component.js.map +1 -1
  4. package/src/lib/components/ai-site-import-start.component.js +3 -3
  5. package/src/lib/components/ai-site-import-start.component.js.map +1 -1
  6. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  7. package/src/lib/components/ai-site-start-card.component.js +129 -29
  8. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  9. package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
  10. package/src/lib/components/ai-site-start-steps.component.js +42 -25
  11. package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
  12. package/src/lib/components/ai-site-style-previews.d.ts +17 -0
  13. package/src/lib/components/ai-site-style-previews.js +26 -95
  14. package/src/lib/components/ai-site-style-previews.js.map +1 -1
  15. package/src/lib/declarations.server.js +22 -0
  16. package/src/lib/declarations.server.js.map +1 -1
  17. package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
  18. package/src/lib/jobs/ai-job-import-step.js +179 -49
  19. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
  21. package/src/lib/jobs/ai-job-layout-language.js +156 -113
  22. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  23. package/src/lib/jobs/ai-job-layout-step.js +56 -17
  24. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  25. package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
  26. package/src/lib/jobs/ai-job-page-language.js +120 -66
  27. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  28. package/src/lib/jobs/ai-job-page-step.js +27 -7
  29. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
  31. package/src/lib/jobs/ai-job-site-datasets.js +36 -16
  32. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  33. package/src/lib/jobs/ai-jobs.js +6 -2
  34. package/src/lib/jobs/ai-jobs.js.map +1 -1
  35. package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
  36. package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
  37. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  38. package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
  39. package/src/lib/jobs/ai-site-import-compile.js +1143 -0
  40. package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
  41. package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
  42. package/src/lib/jobs/ai-site-import-document.js +23 -489
  43. package/src/lib/jobs/ai-site-import-document.js.map +1 -1
  44. package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
  45. package/src/lib/jobs/ai-site-import-look.js +163 -52
  46. package/src/lib/jobs/ai-site-import-look.js.map +1 -1
  47. package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
  48. package/src/lib/jobs/ai-site-import-publish.js +19 -5
  49. package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
  50. package/src/lib/jobs/ai-site-late-photos.js +4 -2
  51. package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +10 -1
  53. package/src/lib/jobs/ai-site-publish.js +8 -3
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
  58. package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
  59. package/src/lib/layout-language/ai-layout-compiler.js +677 -33
  60. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  61. package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
  62. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
  63. package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
  64. package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
  65. package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
  66. package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
  67. package/src/lib/layout-language/ai-layout-frame.js +121 -29
  68. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  69. package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
  70. package/src/lib/layout-language/ai-layout-language.js +85 -120
  71. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  72. package/src/lib/model/ai-credit-estimate.d.ts +11 -7
  73. package/src/lib/model/ai-credit-estimate.js +11 -7
  74. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  75. package/src/lib/model/ai-site-import.d.ts +307 -13
  76. package/src/lib/model/ai-site-import.js +227 -19
  77. package/src/lib/model/ai-site-import.js.map +1 -1
  78. package/src/lib/model/ai-site-look.d.ts +65 -1
  79. package/src/lib/model/ai-site-look.js +141 -23
  80. package/src/lib/model/ai-site-look.js.map +1 -1
  81. package/src/lib/model/ai-site-photos.d.ts +2 -0
  82. package/src/lib/model/ai-site-photos.js.map +1 -1
  83. package/src/lib/model/ai-site-start-steps.d.ts +4 -3
  84. package/src/lib/model/ai-site-start-steps.js +11 -3
  85. package/src/lib/model/ai-site-start-steps.js.map +1 -1
  86. package/src/lib/model/ai-site-start.d.ts +23 -2
  87. package/src/lib/model/ai-site-start.js +25 -3
  88. package/src/lib/model/ai-site-start.js.map +1 -1
  89. package/src/lib/providers/routing.d.ts +11 -0
  90. package/src/lib/providers/routing.js +23 -0
  91. package/src/lib/providers/routing.js.map +1 -1
  92. package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
  93. package/src/lib/runtime/ai-doctrine-validators.js +7 -2
  94. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  95. package/src/lib/runtime/ai-icon-library.d.ts +4 -0
  96. package/src/lib/runtime/ai-icon-library.js +4 -0
  97. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  98. package/src/lib/runtime/ai-node-tree.js +5 -1
  99. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  100. package/src/lib/runtime/ai-palette.d.ts +2 -0
  101. package/src/lib/runtime/ai-palette.generated.js +19 -3
  102. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  103. package/src/lib/runtime/ai-palette.js +2 -1
  104. package/src/lib/runtime/ai-palette.js.map +1 -1
  105. package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
  106. package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
  107. package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
  108. package/src/lib/site-import/site-import-crawl.d.ts +18 -1
  109. package/src/lib/site-import/site-import-crawl.js +70 -10
  110. package/src/lib/site-import/site-import-crawl.js.map +1 -1
  111. package/src/lib/site-import/site-import-css.d.ts +110 -0
  112. package/src/lib/site-import/site-import-css.js +826 -0
  113. package/src/lib/site-import/site-import-css.js.map +1 -0
  114. package/src/lib/site-import/site-import-html.d.ts +116 -1
  115. package/src/lib/site-import/site-import-html.js +538 -46
  116. package/src/lib/site-import/site-import-html.js.map +1 -1
  117. package/src/lib/site-import/site-import-layout.d.ts +79 -0
  118. package/src/lib/site-import/site-import-layout.js +1608 -0
  119. package/src/lib/site-import/site-import-layout.js.map +1 -0
  120. package/src/lib/site-import/site-import-nav.d.ts +40 -0
  121. package/src/lib/site-import/site-import-nav.js +437 -0
  122. package/src/lib/site-import/site-import-nav.js.map +1 -0
  123. package/src/lib/site-import/site-import-page.d.ts +8 -1
  124. package/src/lib/site-import/site-import-page.js +24 -2
  125. package/src/lib/site-import/site-import-page.js.map +1 -1
  126. package/src/lib/assets/site-styles/beauty.webp +0 -0
  127. package/src/lib/assets/site-styles/blog.webp +0 -0
  128. package/src/lib/assets/site-styles/business.webp +0 -0
  129. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  130. package/src/lib/assets/site-styles/education.webp +0 -0
  131. package/src/lib/assets/site-styles/events.webp +0 -0
  132. package/src/lib/assets/site-styles/fitness.webp +0 -0
  133. package/src/lib/assets/site-styles/landing.webp +0 -0
  134. package/src/lib/assets/site-styles/music.webp +0 -0
  135. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  136. package/src/lib/assets/site-styles/personal.webp +0 -0
  137. package/src/lib/assets/site-styles/photography.webp +0 -0
  138. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  139. package/src/lib/assets/site-styles/professional.webp +0 -0
  140. package/src/lib/assets/site-styles/realestate.webp +0 -0
  141. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  142. package/src/lib/assets/site-styles/store.webp +0 -0
  143. package/src/lib/assets/site-styles/studio.webp +0 -0
  144. package/src/lib/assets/site-styles/trades.webp +0 -0
  145. package/src/lib/assets/site-styles/wellness.webp +0 -0
  146. package/src/lib/assets/site-styles/yoga.webp +0 -0
  147. package/src/lib/site-import/site-import-review.d.ts +0 -40
  148. package/src/lib/site-import/site-import-review.js +0 -149
  149. package/src/lib/site-import/site-import-review.js.map +0 -1
@@ -16,6 +16,7 @@
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { aiLayoutDesignChoices } from "../layout-language/ai-layout-design.js";
18
18
  import { aiCompileLayoutFrame } from "../layout-language/ai-layout-frame.js";
19
+ import { aiLayoutFrameSourceOf, aiLayoutFrameSourcePageIds } from "../layout-language/ai-layout-frame-source.js";
19
20
  import { aiLayoutFrameHollows, aiLayoutFrameHollowsSaid, aiLayoutFrameIsHollow, aiLayoutRepairFrameHollows } from "../layout-language/ai-layout-frame-hollows.js";
20
21
  import { AI_LAYOUT_LANGUAGE_TEXT, aiReadLayoutFrame } from "../layout-language/ai-layout-language.js";
21
22
  import { aiLayoutCopyCheck } from "../layout-language/ai-layout-gaps.js";
@@ -69,6 +70,16 @@ import { AI_SITE_NAME_TOKEN } from "./ai-layout-site-facts.js";
69
70
  * and profiles — word for word. Read defensively; `null` on every other job.
70
71
  */ export const AI_LAYOUT_SITE_BRAND_INPUT = 'siteBrand';
71
72
  export const AI_LAYOUT_SITE_FACTS_INPUT = 'siteFacts';
73
+ /**
74
+ * The unit input a site brought in gives its header and footer where the
75
+ * reader found the source's menu (AGL-4108): its menu, its buttons and its
76
+ * footer, every link resolved to a page of the site or the source's own
77
+ * address (`AiLayoutFrameSource`). With it the frame is drawn by code and no
78
+ * model is asked; read defensively, `null` on every other job.
79
+ */ export const AI_LAYOUT_SITE_FRAME_INPUT = 'siteFrame';
80
+ /** A job's source frame (AGL-4108), held to what a link and a label may carry; `null` where it has none. */ export function aiLayoutSiteFrameOf(inputs) {
81
+ return aiLayoutFrameSourceOf(inputs == null ? void 0 : inputs[AI_LAYOUT_SITE_FRAME_INPUT]);
82
+ }
72
83
  /** The most characters of a site's own header and footer words a frame is shown. */ export const AI_LAYOUT_SITE_FACTS_MAX_CHARS = 4000;
73
84
  export function aiLayoutSiteBrandOf(inputs) {
74
85
  const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_SITE_BRAND_INPUT];
@@ -76,13 +87,20 @@ export function aiLayoutSiteBrandOf(inputs) {
76
87
  const value = raw;
77
88
  const logo = value['logo'];
78
89
  const src = typeof (logo == null ? void 0 : logo['src']) === 'string' && logo['src'].trim() ? logo['src'].trim() : null;
90
+ // The logo's own words and shape, as the source names and draws it (AGL-4108).
91
+ const alt = typeof value['alt'] === 'string' ? value['alt'].replace(/\s+/g, ' ').trim().slice(0, 120) : '';
92
+ const aspect = typeof value['aspect'] === 'number' && Number.isFinite(value['aspect']) && value['aspect'] > 0 ? Math.min(value['aspect'], 8) : null;
79
93
  return _extends({}, src ? {
80
94
  logo: {
81
95
  src
82
96
  }
83
97
  } : {}, {
84
98
  name: src ? value['name'] === true : true
85
- });
99
+ }, src && alt ? {
100
+ alt
101
+ } : {}, src && aspect ? {
102
+ aspect
103
+ } : {});
86
104
  }
87
105
  export function aiLayoutSiteFactsOf(inputs) {
88
106
  const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_SITE_FACTS_INPUT];
@@ -196,125 +214,150 @@ export function aiLayoutSiteFactsOf(inputs) {
196
214
  /** The check a frame answer is held to: read, compiled, stored and checked as a layout, with the layout door's own checks. */ export function aiLayoutFrameCheck(input) {
197
215
  let answers = 0;
198
216
  return (answer)=>{
199
- var _input_targets_listings, _input_storeLinks;
200
217
  // The last answer a generation takes has its gaps taken out rather than asked about again.
201
218
  answers += 1;
202
- const last = answers >= AI_GENERATION_MAX_ATTEMPTS;
203
- const frame = aiReadLayoutFrame(answer);
204
- if (input.headerAlign && frame.header) frame.header.align = input.headerAlign;
205
- const compiled = aiCompileLayoutFrame(frame, _extends({
206
- siteName: input.siteName,
207
- homeId: input.homeId,
208
- navPages: input.pages,
209
- closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false,
210
- // A site that sells carries its cart in the header (AGL-3676).
211
- cart: ((_input_targets_listings = input.targets.listings) != null ? _input_targets_listings : []).some((listing)=>listing.kind === 'products' && listing.cart !== false),
212
- // And a store's header is a storefront's: no button repeating its navigation, an announcement bar (AGL-3676).
213
- shopId: aiLayoutShopPageId(input.targets),
214
- // And its account and policies, written after it (AGL-3676).
215
- storeLinks: (_input_storeLinks = input.storeLinks) != null ? _input_storeLinks : null
216
- }, input.language ? {
217
- language: input.language
218
- } : {}, input.brand ? {
219
- brand: input.brand
220
- } : {}), input.targets);
221
- const context = _extends({
222
- screenIds: [
223
- ...new Set([
224
- ...input.targets.pages.map((page)=>page.id),
225
- ...input.pages.map((page)=>page.id),
226
- ...input.homeId ? [
227
- input.homeId
228
- ] : []
229
- ])
230
- ],
231
- formIds: input.targets.forms.map((form)=>form.id),
232
- homeScreenIds: input.targets.homeIds,
233
- // A link home under the home page's own title says home, in any language (AGL-3760).
234
- homeLabels: [
235
- ...input.pages.filter((page)=>aiLayoutIsHomeSlug(page.slug)),
236
- ...input.targets.pages.filter((page)=>input.targets.homeIds.includes(page.id))
237
- ].map((page)=>page.label)
238
- }, input.targets.formPageId ? {
239
- formScreenIds: [
240
- input.targets.formPageId
241
- ]
242
- } : {}, {
243
- codeBuilt: true
219
+ return aiLayoutFrameBuild(aiReadLayoutFrame(answer), input, answers >= AI_GENERATION_MAX_ATTEMPTS);
220
+ };
221
+ }
222
+ /**
223
+ * A site brought in's header and footer as its source builds them
224
+ * (AGL-4108), with no model: compiled, stored and checked as a layout, held
225
+ * to every check an answer is. `value` is `null` with the violations where it
226
+ * does not pass, and the frame is then designed by the model instead.
227
+ */ export function aiLayoutSourceFrameBuild(input) {
228
+ const band = input.source.header === 'brand' || input.source.header === 'dark' ? input.source.header : 'plain';
229
+ return aiLayoutFrameBuild({
230
+ header: {
231
+ band,
232
+ align: 'start',
233
+ blocks: []
234
+ },
235
+ footer: null
236
+ }, input, true);
237
+ }
238
+ /** A frame compiled, stored and checked as a layout; `last` takes its gaps out rather than asking about them. */ function aiLayoutFrameBuild(frame, input, last) {
239
+ var _input_targets_listings, _input_storeLinks;
240
+ if (input.headerAlign && frame.header && !input.source) frame.header.align = input.headerAlign;
241
+ const compiled = aiCompileLayoutFrame(frame, _extends({
242
+ siteName: input.siteName,
243
+ homeId: input.homeId,
244
+ navPages: input.pages,
245
+ closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false,
246
+ // A site that sells carries its cart in the header (AGL-3676).
247
+ cart: ((_input_targets_listings = input.targets.listings) != null ? _input_targets_listings : []).some((listing)=>listing.kind === 'products' && listing.cart !== false),
248
+ // And a store's header is a storefront's: no button repeating its navigation, an announcement bar (AGL-3676).
249
+ shopId: aiLayoutShopPageId(input.targets),
250
+ // And its account and policies, written after it (AGL-3676).
251
+ storeLinks: (_input_storeLinks = input.storeLinks) != null ? _input_storeLinks : null
252
+ }, input.language ? {
253
+ language: input.language
254
+ } : {}, input.brand ? {
255
+ brand: input.brand
256
+ } : {}, input.source ? {
257
+ source: input.source
258
+ } : {}), input.targets);
259
+ // The pages a source's menu and footer link by id (AGL-4108).
260
+ const sourcePages = input.source ? aiLayoutFrameSourcePageIds(input.source) : [];
261
+ const context = _extends({
262
+ screenIds: [
263
+ ...new Set([
264
+ ...input.targets.pages.map((page)=>page.id),
265
+ ...input.pages.map((page)=>page.id),
266
+ ...input.homeId ? [
267
+ input.homeId
268
+ ] : [],
269
+ ...sourcePages
270
+ ])
271
+ ],
272
+ formIds: input.targets.forms.map((form)=>form.id),
273
+ homeScreenIds: input.targets.homeIds,
274
+ // A link home under the home page's own title says home, in any language (AGL-3760).
275
+ homeLabels: [
276
+ ...input.pages.filter((page)=>aiLayoutIsHomeSlug(page.slug)),
277
+ ...input.targets.pages.filter((page)=>input.targets.homeIds.includes(page.id))
278
+ ].map((page)=>page.label)
279
+ }, input.targets.formPageId ? {
280
+ formScreenIds: [
281
+ input.targets.formPageId
282
+ ]
283
+ } : {}, {
284
+ codeBuilt: true
285
+ }, input.source ? {
286
+ ownWords: true,
287
+ reusableComponents: false
288
+ } : {});
289
+ const stored = aiLayoutStoredTree(compiled.tree, 'layout', context);
290
+ if (stored.ok === false) {
291
+ console.error('ai layout frame: a compiled layout did not store', {
292
+ error: stored.error
244
293
  });
245
- const stored = aiLayoutStoredTree(compiled.tree, 'layout', context);
246
- if (stored.ok === false) {
247
- console.error('ai layout frame: a compiled layout did not store', {
248
- error: stored.error
249
- });
250
- return {
251
- value: null,
252
- violations: [
253
- {
254
- rule: null,
255
- code: 'layout-compile',
256
- message: 'This layout could not be built. Try again.',
257
- detail: stored.error
258
- }
259
- ]
260
- };
261
- }
262
- // No gap and no internal reference in the words of a frame every published page shows.
263
- const navPages = [
264
- ...input.targets.pages,
265
- ...input.pages
266
- ];
267
- const copy = aiLayoutCopyCheck(stored.nodes, stored.rootId, {
268
- pages: navPages,
269
- ids: [
270
- ...navPages.map((page)=>page.id),
271
- ...input.targets.forms.map((form)=>form.id)
272
- ]
273
- }, last);
274
- const { dropped } = copy;
275
- // An attribute a rule has one answer for is settled, not asked of the model again (AGL-3660).
276
- const nodes = aiSettleUnrelatedScreenLinks(aiSettleMechanicalRules({
277
- rootId: stored.rootId,
278
- nodes: copy.nodes
279
- }, 'layout', // And on the last answer, a link that goes nowhere, as the check below reads it (AGL-4076).
280
- last ? context : undefined), 'layout', context).nodes;
281
- // The copy check can take words out of the footer: no heading or column
282
- // is left with nothing under it (AGL-3735), and none may remain.
283
- aiLayoutRepairFrameHollows(nodes);
284
- const hollow = aiLayoutFrameHollows(nodes);
285
- const report = validateAiDoctrineTree({
286
- rootId: stored.rootId,
287
- nodes
288
- }, 'layout', context);
289
- const violations = [
290
- ...report.violations,
291
- ...aiLayoutFrameIsHollow(hollow) ? [
294
+ return {
295
+ value: null,
296
+ violations: [
292
297
  {
293
298
  rule: null,
294
- code: 'layout-frame-hollow',
295
- message: 'Every footer heading needs something under it, and every footer column more than a heading.',
296
- detail: aiLayoutFrameHollowsSaid(hollow).join('; ')
299
+ code: 'layout-compile',
300
+ message: 'This layout could not be built. Try again.',
301
+ detail: stored.error
297
302
  }
298
- ] : [],
299
- ...copy.violations,
300
- ...input.extend({
301
- rootId: stored.rootId,
302
- nodes,
303
- repairs: [],
304
- sourceIds: {},
305
- score: report.score,
306
- load: report.load
307
- })
308
- ];
309
- return {
310
- value: violations.length ? null : {
311
- rootId: stored.rootId,
312
- nodes,
313
- load: report.load,
314
- dropped
315
- },
316
- violations
303
+ ]
317
304
  };
305
+ }
306
+ // No gap and no internal reference in the words of a frame every published page shows.
307
+ const navPages = [
308
+ ...input.targets.pages,
309
+ ...input.pages
310
+ ];
311
+ const copy = aiLayoutCopyCheck(stored.nodes, stored.rootId, {
312
+ pages: navPages,
313
+ ids: [
314
+ ...navPages.map((page)=>page.id),
315
+ ...input.targets.forms.map((form)=>form.id)
316
+ ]
317
+ }, last);
318
+ const { dropped } = copy;
319
+ // An attribute a rule has one answer for is settled, not asked of the model again (AGL-3660).
320
+ const nodes = aiSettleUnrelatedScreenLinks(aiSettleMechanicalRules({
321
+ rootId: stored.rootId,
322
+ nodes: copy.nodes
323
+ }, 'layout', // And on the last answer, a link that goes nowhere, as the check below reads it (AGL-4076).
324
+ last ? context : undefined), 'layout', context).nodes;
325
+ // The copy check can take words out of the footer: no heading or column
326
+ // is left with nothing under it (AGL-3735), and none may remain.
327
+ aiLayoutRepairFrameHollows(nodes);
328
+ const hollow = aiLayoutFrameHollows(nodes);
329
+ const report = validateAiDoctrineTree({
330
+ rootId: stored.rootId,
331
+ nodes
332
+ }, 'layout', context);
333
+ const violations = [
334
+ ...report.violations,
335
+ ...aiLayoutFrameIsHollow(hollow) ? [
336
+ {
337
+ rule: null,
338
+ code: 'layout-frame-hollow',
339
+ message: 'Every footer heading needs something under it, and every footer column more than a heading.',
340
+ detail: aiLayoutFrameHollowsSaid(hollow).join('; ')
341
+ }
342
+ ] : [],
343
+ ...copy.violations,
344
+ ...input.extend({
345
+ rootId: stored.rootId,
346
+ nodes,
347
+ repairs: [],
348
+ sourceIds: {},
349
+ score: report.score,
350
+ load: report.load
351
+ })
352
+ ];
353
+ return {
354
+ value: violations.length ? null : {
355
+ rootId: stored.rootId,
356
+ nodes,
357
+ load: report.load,
358
+ dropped
359
+ },
360
+ violations
318
361
  };
319
362
  }
320
363
  /** What a frame's links resolve against: the site's pages and its form, as a page's do. */ export function aiLayoutFrameTargets(job, inventory) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-language.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 type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutDesignChoices, type AiLayoutDesign } from '../layout-language/ai-layout-design'\nimport { aiCompileLayoutFrame, type AiLayoutFrameBrand } from '../layout-language/ai-layout-frame'\nimport {\n aiLayoutFrameHollows,\n aiLayoutFrameHollowsSaid,\n aiLayoutFrameIsHollow,\n aiLayoutRepairFrameHollows,\n} from '../layout-language/ai-layout-frame-hollows'\nimport {\n AI_LAYOUT_FRAME_TOOL,\n AI_LAYOUT_LANGUAGE_TEXT,\n aiReadLayoutFrame,\n} from '../layout-language/ai-layout-language'\nimport type {\n AiLayoutPage,\n AiLayoutTargets,\n} from '../layout-language/ai-layout-links'\nimport { aiLayoutCopyCheck } from '../layout-language/ai-layout-gaps'\nimport { aiLayoutStoredTree } from '../layout-language/ai-layout-store'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport type { AiJob, AiJobPlan } from '../model/ai-jobs.types'\nimport { aiLayoutSpeaksOtherThanEnglish } from '../layout-language/ai-layout-labels'\nimport {\n aiHomeScreenIds,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport type { AiGenerationCheck, AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n aiSettleMechanicalRules,\n aiSettleUnrelatedScreenLinks,\n validateAiDoctrineTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { aiSiteKindDesignLines } from '../model/ai-site-kinds'\nimport type { AiStoreFrameLinks } from '../model/ai-site-store-pages'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { fenceUntrustedText, UNTRUSTED_TEXT_RULE } from '@aglyn/shared-util-tools/untrusted-text'\nimport { aiJobBriefLine } from './ai-job-generation'\nimport {\n AI_LAYOUT_SITE_PAGES_MAX,\n aiLayoutIsHomeSlug,\n aiLayoutSitePages,\n} from './ai-job-layout-site-pages'\nimport {\n AI_LAYOUT_FRAME_THINKING_TOKENS,\n aiLayoutPageTargets,\n} from './ai-job-page-language'\nimport { AI_SITE_NAME_TOKEN } from './ai-layout-site-facts'\n\n/**\n * A site's layout built in the compact layout language (AGL-3660): the layout\n * step's pass for a guided start's and a build's layout unit. The model\n * designs the header and the footer — their bands, the call to action, the\n * footer's columns and every word — in one short answer through\n * `submit_frame`, and the compiler writes the layout around what the site\n * owes it: its name as the brand, its pages as the navigation and the phone\n * menu, the Layout Slot, and the footer at the bottom of a short page\n * (`ai-layout-frame.ts`).\n */\n\n/**\n * The most a frame's answer asks for: two sections of a few blocks each (the\n * live runs of 2026-10-07 wrote 442 to 583), and the thinking before them.\n */\nexport const AI_JOB_LAYOUT_LANGUAGE_TOKENS = 1_200 + AI_LAYOUT_FRAME_THINKING_TOKENS\n\n/** A language layout pass's time, on the tier the layout step is served from. */\nexport const AI_JOB_LAYOUT_LANGUAGE_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_LANGUAGE_TOKENS,\n})\n\n/** The generation kind a frame is asked under, which the doctrine scopes. */\nexport const AI_LAYOUT_FRAME_KIND = 'layout-frame'\n\n/** The language, as the frame door's cached instructions. */\nexport const AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n AI_LAYOUT_LANGUAGE_TEXT,\n '',\n \"Now you design a site's header and footer, each one section of the language, answered with submit_frame.\",\n \"- header: the platform writes the site's name and its navigation itself. Choose its band and align (start, or center for a centered navigation), and give it at most one button: the call to action every page shows.\",\n '- footer: a section like any other, usually a row of two or three columns: a few words about the business, the details the brief gives, and a list of links (items with to). Put no contact detail, opening hours or address in it that the brief does not give, and no gap in square brackets for one.',\n ].join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/**\n * The unit input a site brought in from elsewhere gives its header and\n * footer (AGL-4084): its header's brand as the source draws it, and the words\n * the source's header and footer carry — its address, phone, email, hours\n * and profiles — word for word. Read defensively; `null` on every other job.\n */\nexport const AI_LAYOUT_SITE_BRAND_INPUT = 'siteBrand'\nexport const AI_LAYOUT_SITE_FACTS_INPUT = 'siteFacts'\n\n/** The most characters of a site's own header and footer words a frame is shown. */\nexport const AI_LAYOUT_SITE_FACTS_MAX_CHARS = 4_000\n\nexport interface AiLayoutSiteFacts {\n /** Where they were read: the owner's own home page. */\n url: string\n /** The site's own description of itself. */\n description: string\n /** Its header's and footer's lines, as written. */\n lines: string[]\n}\n\nexport function aiLayoutSiteBrandOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutFrameBrand | null {\n const raw = inputs?.[AI_LAYOUT_SITE_BRAND_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const value = raw as Record<string, unknown>\n const logo = value['logo'] as Record<string, unknown> | null | undefined\n const src = typeof logo?.['src'] === 'string' && logo['src'].trim() ? logo['src'].trim() : null\n return { ...(src ? { logo: { src } } : {}), name: src ? value['name'] === true : true }\n}\n\nexport function aiLayoutSiteFactsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutSiteFacts | null {\n const raw = inputs?.[AI_LAYOUT_SITE_FACTS_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const value = raw as Record<string, unknown>\n const lines = Array.isArray(value['lines']) ? value['lines'].filter((line): line is string => typeof line === 'string' && !!line.trim()) : []\n const description = typeof value['description'] === 'string' ? value['description'] : ''\n if (!lines.length && !description.trim()) return null\n return { url: typeof value['url'] === 'string' ? value['url'].slice(0, 2048) : '', description, lines }\n}\n\n/** A site's own header and footer words as one text, which its frame's contact details are held to (AGL-4084). */\nexport function aiLayoutSiteFactsText(inputs: Readonly<Record<string, unknown>> | null | undefined): string {\n const facts = aiLayoutSiteFactsOf(inputs)\n return facts ? [facts.description, ...facts.lines].filter(Boolean).join('\\n') : ''\n}\n\n/**\n * What a frame brought in from the owner's own site is told (AGL-4084): its\n * footer carries the details the source's header and footer give, as they\n * give them, and nothing else; the words themselves, fenced as data.\n */\nexport function aiLayoutSiteFactsLines(facts: AiLayoutSiteFacts | null): string[] {\n if (!facts) return []\n const text = [facts.description ? `About the site, in its own words: ${facts.description}` : '', ...facts.lines].filter(Boolean).join('\\n')\n return [\n \"This site's header and footer on its owner's current website carry the words below. Put in the footer the details they give (the address, phone number, email, hours and profiles), written exactly as they are written there, and its own words about itself; invent nothing they do not give, and print no word of these instructions.\",\n fenceUntrustedText(`The header and footer of ${facts.url || \"the owner's site\"}`, text, { maxChars: AI_LAYOUT_SITE_FACTS_MAX_CHARS }),\n ]\n}\n\n/** The frame door's system blocks: the language's, and for a site brought in the rule that fenced text is data (AGL-4084). */\nexport function aiJobLayoutLanguageInstructions(job: Pick<AiJob, 'inputs'>): readonly AiSystemBlock[] {\n return aiLayoutSiteFactsOf(job.inputs)\n ? [...AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS, { text: UNTRUSTED_TEXT_RULE }]\n : AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS\n}\n\n/** The site's name as the brand: the name the job was given, else the token the site's own name fills. */\nexport function aiLayoutSiteName(job: Pick<AiJob, 'inputs'>): string {\n const given = job.inputs?.['businessName']\n return typeof given === 'string' && given.trim()\n ? given.trim()\n : AI_SITE_NAME_TOKEN\n}\n\n/** The pages a frame's navigation links: the planned pages, else the site's own, in order. */\nexport function aiLayoutNavPages(\n job: Pick<AiJob, 'inputs'> & { plan?: Pick<AiJobPlan, 'language'> | null },\n inventory: AiSiteInventory | null,\n): AiLayoutPage[] {\n // The header and the footer link the pages the job was told (AGL-3660),\n // else the site's own, the home page always and first, labelled Home. The\n // model draws neither list, so it cannot drop a page. A site in another\n // language keeps its home page's own title: a Spanish start's \"Inicio\" was\n // linked as \"Home\" (AGL-3760).\n const planned = aiLayoutSitePages(job.inputs)\n const own = (inventory?.screens ?? [])\n .filter((row) => !row.template)\n .map((row) => ({ id: row.id, label: row.name, slug: row.slug }))\n const siteHome = planned.some((page) => aiLayoutIsHomeSlug(page.slug))\n ? []\n : own.filter((page) => aiLayoutIsHomeSlug(page.slug)).slice(0, 1)\n const pages = [...siteHome, ...(planned.length ? planned : own)].filter((page) => page.label.trim())\n const home = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return [\n ...(home ? [aiLayoutSpeaksOtherThanEnglish(job.plan?.language) ? home : { ...home, label: 'Home' }] : []),\n ...pages.filter((page) => page !== home && !aiLayoutIsHomeSlug(page.slug)),\n ].slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/** The home page the brand links: the planned home, else the site's. */\nexport function aiLayoutHomeId(\n pages: readonly AiLayoutPage[],\n inventory: AiSiteInventory | null,\n): string | null {\n const planned = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return planned?.id ?? aiHomeScreenIds(inventory)[0] ?? null\n}\n\n/** The frame's user turn: the site, its brief, its pages and its form. */\nexport function aiLayoutFramePrompt(input: {\n job: Pick<AiJob, 'brief' | '$id'> & Partial<Pick<AiJob, 'inputs'>>\n siteName: string\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n}): string {\n const { job, pages, targets } = input\n // A site brought in (AGL-4084) is told its own words, fenced, and no brief a footer could print.\n const facts = aiLayoutSiteFactsOf(job.inputs)\n return [\n `Site: ${input.siteName === AI_SITE_NAME_TOKEN ? 'the business the brief describes' : `\"${input.siteName}\"`}`,\n ...(facts ? aiLayoutSiteFactsLines(facts) : [aiJobBriefLine(job)]),\n ...aiSiteKindDesignLines(job.inputs),\n pages.length\n ? `Its pages, which the navigation links: ${pages.map((page) => `${page.label} (page:${page.id})`).join(', ')}.`\n : 'It has no pages yet.',\n targets.forms.length || targets.formPageId\n ? 'A call to action that asks a visitor to get in touch goes to form.'\n : 'The site has no form yet.',\n // A storefront's header (AGL-3676): its cart, and an announcement line.\n ...(aiLayoutShopPageId(targets)\n ? [\n \"This is an online store: give the header no button, since the platform puts the cart beside the navigation. Give the header one note instead: a short announcement for the bar above it, under 60 characters, about the shop from the brief (a promise it makes, such as its shipping, or what is new); never invent an offer, a discount, a price or a shipping threshold the brief does not give.\",\n ]\n : []),\n `Design seed: ${job.$id.slice(-6)}.`,\n ].join('\\n')\n}\n\n/** A store's Shop page, by its id: the page its catalog's whole list is placed on (AGL-3676). */\nexport function aiLayoutShopPageId(targets: Pick<AiLayoutTargets, 'listings'>): string | null {\n const products = (targets.listings ?? []).find((listing) => listing.kind === 'products')\n return products?.placements.find((placement) => placement.role === 'index')?.screenId ?? null\n}\n\n/** A validated language layout. */\nexport interface AiLayoutFrameBuilt {\n rootId: string\n nodes: NodesMap\n load: AiLoadEstimate | null\n /** What the last answer's gaps took out of the frame (`ai-layout-gaps.ts`). */\n dropped: string[]\n}\n\n/** The check a frame answer is held to: read, compiled, stored and checked as a layout, with the layout door's own checks. */\nexport function aiLayoutFrameCheck(input: {\n /** The header arrangement the site's look chose (AGL-3660); the answer's otherwise. */\n headerAlign?: 'start' | 'center'\n siteName: string\n homeId: string | null\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n extend: (tree: AiValidatedTree) => AiDoctrineViolation[]\n /** The site's design (`aiLayoutDesignOf`), whose pages' closing band the footer sits under. */\n design?: AiLayoutDesign | null\n /** A store's own pages, which the header and footer link by path (AGL-3676). */\n storeLinks?: AiStoreFrameLinks | null\n /** The site's language, which the frame's own words are written in (AGL-3760). */\n language?: string\n /** The header's brand as a site brought in draws it (AGL-4084). */\n brand?: AiLayoutFrameBrand | null\n}): AiGenerationCheck<AiLayoutFrameBuilt> {\n let answers = 0\n return (answer) => {\n // The last answer a generation takes has its gaps taken out rather than asked about again.\n answers += 1\n const last = answers >= AI_GENERATION_MAX_ATTEMPTS\n const frame = aiReadLayoutFrame(answer)\n if (input.headerAlign && frame.header) frame.header.align = input.headerAlign\n const compiled = aiCompileLayoutFrame(\n frame,\n {\n siteName: input.siteName,\n homeId: input.homeId,\n navPages: input.pages,\n closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false,\n // A site that sells carries its cart in the header (AGL-3676).\n cart: (input.targets.listings ?? []).some((listing) => listing.kind === 'products' && listing.cart !== false),\n // And a store's header is a storefront's: no button repeating its navigation, an announcement bar (AGL-3676).\n shopId: aiLayoutShopPageId(input.targets),\n // And its account and policies, written after it (AGL-3676).\n storeLinks: input.storeLinks ?? null,\n ...(input.language ? { language: input.language } : {}),\n ...(input.brand ? { brand: input.brand } : {}),\n },\n input.targets,\n )\n const context = {\n screenIds: [\n ...new Set([\n ...input.targets.pages.map((page) => page.id),\n ...input.pages.map((page) => page.id),\n ...(input.homeId ? [input.homeId] : []),\n ]),\n ],\n formIds: input.targets.forms.map((form) => form.id),\n homeScreenIds: input.targets.homeIds,\n // A link home under the home page's own title says home, in any language (AGL-3760).\n homeLabels: [\n ...input.pages.filter((page) => aiLayoutIsHomeSlug(page.slug)),\n ...input.targets.pages.filter((page) => input.targets.homeIds.includes(page.id)),\n ].map((page) => page.label),\n // A footer's \"Get in touch\" goes to the form, wherever it is placed (AGL-3729).\n ...(input.targets.formPageId ? { formScreenIds: [input.targets.formPageId] } : {}),\n codeBuilt: true,\n }\n const stored = aiLayoutStoredTree(compiled.tree, 'layout', context)\n if (stored.ok === false) {\n console.error('ai layout frame: a compiled layout did not store', {\n error: stored.error,\n })\n return {\n value: null,\n violations: [\n {\n rule: null,\n code: 'layout-compile',\n message: 'This layout could not be built. Try again.',\n detail: stored.error,\n },\n ],\n }\n }\n // No gap and no internal reference in the words of a frame every published page shows.\n const navPages = [...input.targets.pages, ...input.pages]\n const copy = aiLayoutCopyCheck(\n stored.nodes,\n stored.rootId,\n {\n pages: navPages,\n ids: [...navPages.map((page) => page.id), ...input.targets.forms.map((form) => form.id)],\n },\n last,\n )\n const { dropped } = copy\n // An attribute a rule has one answer for is settled, not asked of the model again (AGL-3660).\n const nodes = aiSettleUnrelatedScreenLinks(\n aiSettleMechanicalRules(\n { rootId: stored.rootId, nodes: copy.nodes as unknown as Record<string, AiDoctrineNode> },\n 'layout',\n // And on the last answer, a link that goes nowhere, as the check below reads it (AGL-4076).\n last ? context : undefined,\n ),\n 'layout',\n context,\n ).nodes as unknown as typeof copy.nodes\n // The copy check can take words out of the footer: no heading or column\n // is left with nothing under it (AGL-3735), and none may remain.\n aiLayoutRepairFrameHollows(nodes as unknown as Record<string, unknown>)\n const hollow = aiLayoutFrameHollows(nodes as unknown as Record<string, unknown>)\n const report = validateAiDoctrineTree(\n { rootId: stored.rootId, nodes },\n 'layout',\n context,\n )\n const violations = [\n ...report.violations,\n ...(aiLayoutFrameIsHollow(hollow)\n ? [\n {\n rule: null,\n code: 'layout-frame-hollow',\n message: 'Every footer heading needs something under it, and every footer column more than a heading.',\n detail: aiLayoutFrameHollowsSaid(hollow).join('; '),\n },\n ]\n : []),\n ...copy.violations,\n ...input.extend({\n rootId: stored.rootId,\n nodes,\n repairs: [],\n sourceIds: {},\n score: report.score as AiValidatedTree['score'],\n load: report.load,\n }),\n ]\n return {\n value: violations.length\n ? null\n : { rootId: stored.rootId, nodes, load: report.load, dropped },\n violations,\n }\n }\n}\n\n/** What a frame's links resolve against: the site's pages and its form, as a page's do. */\nexport function aiLayoutFrameTargets(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutTargets {\n return aiLayoutPageTargets({ job, inventory, own: [] })\n}\n"],"names":["aiLayoutDesignChoices","aiCompileLayoutFrame","aiLayoutFrameHollows","aiLayoutFrameHollowsSaid","aiLayoutFrameIsHollow","aiLayoutRepairFrameHollows","AI_LAYOUT_LANGUAGE_TEXT","aiReadLayoutFrame","aiLayoutCopyCheck","aiLayoutStoredTree","AI_GENERATION_MAX_ATTEMPTS","aiLayoutSpeaksOtherThanEnglish","aiHomeScreenIds","AI_STEP_TIERS","aiSettleMechanicalRules","aiSettleUnrelatedScreenLinks","validateAiDoctrineTree","aiSiteKindDesignLines","aiJobStepBudget","fenceUntrustedText","UNTRUSTED_TEXT_RULE","aiJobBriefLine","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","aiLayoutSitePages","AI_LAYOUT_FRAME_THINKING_TOKENS","aiLayoutPageTargets","AI_SITE_NAME_TOKEN","AI_JOB_LAYOUT_LANGUAGE_TOKENS","AI_JOB_LAYOUT_LANGUAGE_BUDGET","tier","maxTokens","AI_LAYOUT_FRAME_KIND","AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS","text","join","cacheBreakpoint","AI_LAYOUT_SITE_BRAND_INPUT","AI_LAYOUT_SITE_FACTS_INPUT","AI_LAYOUT_SITE_FACTS_MAX_CHARS","aiLayoutSiteBrandOf","inputs","raw","value","logo","src","trim","name","aiLayoutSiteFactsOf","lines","Array","isArray","filter","line","description","length","url","slice","aiLayoutSiteFactsText","facts","Boolean","aiLayoutSiteFactsLines","maxChars","aiJobLayoutLanguageInstructions","job","aiLayoutSiteName","given","aiLayoutNavPages","inventory","planned","own","screens","row","template","map","id","label","slug","siteHome","some","page","pages","home","find","plan","language","aiLayoutHomeId","aiLayoutFramePrompt","input","targets","siteName","forms","formPageId","aiLayoutShopPageId","$id","products","listings","listing","kind","placements","placement","role","screenId","aiLayoutFrameCheck","answers","answer","last","frame","headerAlign","header","align","compiled","homeId","navPages","closesDark","design","coverClose","cart","shopId","storeLinks","brand","context","screenIds","Set","formIds","form","homeScreenIds","homeIds","homeLabels","includes","formScreenIds","codeBuilt","stored","tree","ok","console","error","violations","rule","code","message","detail","copy","nodes","rootId","ids","dropped","undefined","hollow","report","extend","repairs","sourceIds","score","load","aiLayoutFrameTargets"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-layout-language.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 type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiLayoutDesignChoices, type AiLayoutDesign } from '../layout-language/ai-layout-design'\nimport { aiCompileLayoutFrame, type AiLayoutFrameBrand } from '../layout-language/ai-layout-frame'\nimport { aiLayoutFrameSourceOf, aiLayoutFrameSourcePageIds, type AiLayoutFrameSource } from '../layout-language/ai-layout-frame-source'\nimport {\n aiLayoutFrameHollows,\n aiLayoutFrameHollowsSaid,\n aiLayoutFrameIsHollow,\n aiLayoutRepairFrameHollows,\n} from '../layout-language/ai-layout-frame-hollows'\nimport {\n AI_LAYOUT_FRAME_TOOL,\n AI_LAYOUT_LANGUAGE_TEXT,\n aiReadLayoutFrame,\n type AiLayoutSection,\n} from '../layout-language/ai-layout-language'\nimport type {\n AiLayoutPage,\n AiLayoutTargets,\n} from '../layout-language/ai-layout-links'\nimport { aiLayoutCopyCheck } from '../layout-language/ai-layout-gaps'\nimport { aiLayoutStoredTree } from '../layout-language/ai-layout-store'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport type { AiJob, AiJobPlan } from '../model/ai-jobs.types'\nimport { aiLayoutSpeaksOtherThanEnglish } from '../layout-language/ai-layout-labels'\nimport {\n aiHomeScreenIds,\n type AiSiteInventory,\n} from '../model/ai-site-inventory'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport type { AiGenerationCheck, AiValidatedTree } from '../runtime/ai-doctrine'\nimport {\n aiSettleMechanicalRules,\n aiSettleUnrelatedScreenLinks,\n validateAiDoctrineTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { aiSiteKindDesignLines } from '../model/ai-site-kinds'\nimport type { AiStoreFrameLinks } from '../model/ai-site-store-pages'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { fenceUntrustedText, UNTRUSTED_TEXT_RULE } from '@aglyn/shared-util-tools/untrusted-text'\nimport { aiJobBriefLine } from './ai-job-generation'\nimport {\n AI_LAYOUT_SITE_PAGES_MAX,\n aiLayoutIsHomeSlug,\n aiLayoutSitePages,\n} from './ai-job-layout-site-pages'\nimport {\n AI_LAYOUT_FRAME_THINKING_TOKENS,\n aiLayoutPageTargets,\n} from './ai-job-page-language'\nimport { AI_SITE_NAME_TOKEN } from './ai-layout-site-facts'\n\n/**\n * A site's layout built in the compact layout language (AGL-3660): the layout\n * step's pass for a guided start's and a build's layout unit. The model\n * designs the header and the footer — their bands, the call to action, the\n * footer's columns and every word — in one short answer through\n * `submit_frame`, and the compiler writes the layout around what the site\n * owes it: its name as the brand, its pages as the navigation and the phone\n * menu, the Layout Slot, and the footer at the bottom of a short page\n * (`ai-layout-frame.ts`).\n */\n\n/**\n * The most a frame's answer asks for: two sections of a few blocks each (the\n * live runs of 2026-10-07 wrote 442 to 583), and the thinking before them.\n */\nexport const AI_JOB_LAYOUT_LANGUAGE_TOKENS = 1_200 + AI_LAYOUT_FRAME_THINKING_TOKENS\n\n/** A language layout pass's time, on the tier the layout step is served from. */\nexport const AI_JOB_LAYOUT_LANGUAGE_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.layout'],\n maxTokens: AI_JOB_LAYOUT_LANGUAGE_TOKENS,\n})\n\n/** The generation kind a frame is asked under, which the doctrine scopes. */\nexport const AI_LAYOUT_FRAME_KIND = 'layout-frame'\n\n/** The language, as the frame door's cached instructions. */\nexport const AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n AI_LAYOUT_LANGUAGE_TEXT,\n '',\n \"Now you design a site's header and footer, each one section of the language, answered with submit_frame.\",\n \"- header: the platform writes the site's name and its navigation itself. Choose its band and align (start, or center for a centered navigation), and give it at most one button: the call to action every page shows.\",\n '- footer: a section like any other, usually a row of two or three columns: a few words about the business, the details the brief gives, and a list of links (items with to). Put no contact detail, opening hours or address in it that the brief does not give, and no gap in square brackets for one.',\n ].join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/**\n * The unit input a site brought in from elsewhere gives its header and\n * footer (AGL-4084): its header's brand as the source draws it, and the words\n * the source's header and footer carry — its address, phone, email, hours\n * and profiles — word for word. Read defensively; `null` on every other job.\n */\nexport const AI_LAYOUT_SITE_BRAND_INPUT = 'siteBrand'\nexport const AI_LAYOUT_SITE_FACTS_INPUT = 'siteFacts'\n\n/**\n * The unit input a site brought in gives its header and footer where the\n * reader found the source's menu (AGL-4108): its menu, its buttons and its\n * footer, every link resolved to a page of the site or the source's own\n * address (`AiLayoutFrameSource`). With it the frame is drawn by code and no\n * model is asked; read defensively, `null` on every other job.\n */\nexport const AI_LAYOUT_SITE_FRAME_INPUT = 'siteFrame'\n\n/** A job's source frame (AGL-4108), held to what a link and a label may carry; `null` where it has none. */\nexport function aiLayoutSiteFrameOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutFrameSource | null {\n return aiLayoutFrameSourceOf(inputs?.[AI_LAYOUT_SITE_FRAME_INPUT])\n}\n\n/** The most characters of a site's own header and footer words a frame is shown. */\nexport const AI_LAYOUT_SITE_FACTS_MAX_CHARS = 4_000\n\nexport interface AiLayoutSiteFacts {\n /** Where they were read: the owner's own home page. */\n url: string\n /** The site's own description of itself. */\n description: string\n /** Its header's and footer's lines, as written. */\n lines: string[]\n}\n\nexport function aiLayoutSiteBrandOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutFrameBrand | null {\n const raw = inputs?.[AI_LAYOUT_SITE_BRAND_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const value = raw as Record<string, unknown>\n const logo = value['logo'] as Record<string, unknown> | null | undefined\n const src = typeof logo?.['src'] === 'string' && logo['src'].trim() ? logo['src'].trim() : null\n // The logo's own words and shape, as the source names and draws it (AGL-4108).\n const alt = typeof value['alt'] === 'string' ? value['alt'].replace(/\\s+/g, ' ').trim().slice(0, 120) : ''\n const aspect = typeof value['aspect'] === 'number' && Number.isFinite(value['aspect']) && value['aspect'] > 0 ? Math.min(value['aspect'], 8) : null\n return { ...(src ? { logo: { src } } : {}), name: src ? value['name'] === true : true, ...(src && alt ? { alt } : {}), ...(src && aspect ? { aspect } : {}) }\n}\n\nexport function aiLayoutSiteFactsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiLayoutSiteFacts | null {\n const raw = inputs?.[AI_LAYOUT_SITE_FACTS_INPUT]\n if (!raw || typeof raw !== 'object') return null\n const value = raw as Record<string, unknown>\n const lines = Array.isArray(value['lines']) ? value['lines'].filter((line): line is string => typeof line === 'string' && !!line.trim()) : []\n const description = typeof value['description'] === 'string' ? value['description'] : ''\n if (!lines.length && !description.trim()) return null\n return { url: typeof value['url'] === 'string' ? value['url'].slice(0, 2048) : '', description, lines }\n}\n\n/** A site's own header and footer words as one text, which its frame's contact details are held to (AGL-4084). */\nexport function aiLayoutSiteFactsText(inputs: Readonly<Record<string, unknown>> | null | undefined): string {\n const facts = aiLayoutSiteFactsOf(inputs)\n return facts ? [facts.description, ...facts.lines].filter(Boolean).join('\\n') : ''\n}\n\n/**\n * What a frame brought in from the owner's own site is told (AGL-4084): its\n * footer carries the details the source's header and footer give, as they\n * give them, and nothing else; the words themselves, fenced as data.\n */\nexport function aiLayoutSiteFactsLines(facts: AiLayoutSiteFacts | null): string[] {\n if (!facts) return []\n const text = [facts.description ? `About the site, in its own words: ${facts.description}` : '', ...facts.lines].filter(Boolean).join('\\n')\n return [\n \"This site's header and footer on its owner's current website carry the words below. Put in the footer the details they give (the address, phone number, email, hours and profiles), written exactly as they are written there, and its own words about itself; invent nothing they do not give, and print no word of these instructions.\",\n fenceUntrustedText(`The header and footer of ${facts.url || \"the owner's site\"}`, text, { maxChars: AI_LAYOUT_SITE_FACTS_MAX_CHARS }),\n ]\n}\n\n/** The frame door's system blocks: the language's, and for a site brought in the rule that fenced text is data (AGL-4084). */\nexport function aiJobLayoutLanguageInstructions(job: Pick<AiJob, 'inputs'>): readonly AiSystemBlock[] {\n return aiLayoutSiteFactsOf(job.inputs)\n ? [...AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS, { text: UNTRUSTED_TEXT_RULE }]\n : AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS\n}\n\n/** The site's name as the brand: the name the job was given, else the token the site's own name fills. */\nexport function aiLayoutSiteName(job: Pick<AiJob, 'inputs'>): string {\n const given = job.inputs?.['businessName']\n return typeof given === 'string' && given.trim()\n ? given.trim()\n : AI_SITE_NAME_TOKEN\n}\n\n/** The pages a frame's navigation links: the planned pages, else the site's own, in order. */\nexport function aiLayoutNavPages(\n job: Pick<AiJob, 'inputs'> & { plan?: Pick<AiJobPlan, 'language'> | null },\n inventory: AiSiteInventory | null,\n): AiLayoutPage[] {\n // The header and the footer link the pages the job was told (AGL-3660),\n // else the site's own, the home page always and first, labelled Home. The\n // model draws neither list, so it cannot drop a page. A site in another\n // language keeps its home page's own title: a Spanish start's \"Inicio\" was\n // linked as \"Home\" (AGL-3760).\n const planned = aiLayoutSitePages(job.inputs)\n const own = (inventory?.screens ?? [])\n .filter((row) => !row.template)\n .map((row) => ({ id: row.id, label: row.name, slug: row.slug }))\n const siteHome = planned.some((page) => aiLayoutIsHomeSlug(page.slug))\n ? []\n : own.filter((page) => aiLayoutIsHomeSlug(page.slug)).slice(0, 1)\n const pages = [...siteHome, ...(planned.length ? planned : own)].filter((page) => page.label.trim())\n const home = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return [\n ...(home ? [aiLayoutSpeaksOtherThanEnglish(job.plan?.language) ? home : { ...home, label: 'Home' }] : []),\n ...pages.filter((page) => page !== home && !aiLayoutIsHomeSlug(page.slug)),\n ].slice(0, AI_LAYOUT_SITE_PAGES_MAX)\n}\n\n/** The home page the brand links: the planned home, else the site's. */\nexport function aiLayoutHomeId(\n pages: readonly AiLayoutPage[],\n inventory: AiSiteInventory | null,\n): string | null {\n const planned = pages.find((page) => aiLayoutIsHomeSlug(page.slug))\n return planned?.id ?? aiHomeScreenIds(inventory)[0] ?? null\n}\n\n/** The frame's user turn: the site, its brief, its pages and its form. */\nexport function aiLayoutFramePrompt(input: {\n job: Pick<AiJob, 'brief' | '$id'> & Partial<Pick<AiJob, 'inputs'>>\n siteName: string\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n}): string {\n const { job, pages, targets } = input\n // A site brought in (AGL-4084) is told its own words, fenced, and no brief a footer could print.\n const facts = aiLayoutSiteFactsOf(job.inputs)\n return [\n `Site: ${input.siteName === AI_SITE_NAME_TOKEN ? 'the business the brief describes' : `\"${input.siteName}\"`}`,\n ...(facts ? aiLayoutSiteFactsLines(facts) : [aiJobBriefLine(job)]),\n ...aiSiteKindDesignLines(job.inputs),\n pages.length\n ? `Its pages, which the navigation links: ${pages.map((page) => `${page.label} (page:${page.id})`).join(', ')}.`\n : 'It has no pages yet.',\n targets.forms.length || targets.formPageId\n ? 'A call to action that asks a visitor to get in touch goes to form.'\n : 'The site has no form yet.',\n // A storefront's header (AGL-3676): its cart, and an announcement line.\n ...(aiLayoutShopPageId(targets)\n ? [\n \"This is an online store: give the header no button, since the platform puts the cart beside the navigation. Give the header one note instead: a short announcement for the bar above it, under 60 characters, about the shop from the brief (a promise it makes, such as its shipping, or what is new); never invent an offer, a discount, a price or a shipping threshold the brief does not give.\",\n ]\n : []),\n `Design seed: ${job.$id.slice(-6)}.`,\n ].join('\\n')\n}\n\n/** A store's Shop page, by its id: the page its catalog's whole list is placed on (AGL-3676). */\nexport function aiLayoutShopPageId(targets: Pick<AiLayoutTargets, 'listings'>): string | null {\n const products = (targets.listings ?? []).find((listing) => listing.kind === 'products')\n return products?.placements.find((placement) => placement.role === 'index')?.screenId ?? null\n}\n\n/** A validated language layout. */\nexport interface AiLayoutFrameBuilt {\n rootId: string\n nodes: NodesMap\n load: AiLoadEstimate | null\n /** What the last answer's gaps took out of the frame (`ai-layout-gaps.ts`). */\n dropped: string[]\n}\n\n/** What a frame is built with: the site, its pages and links, its look, and the layout door's own checks. */\nexport interface AiLayoutFrameInput {\n /** The header arrangement the site's look chose (AGL-3660); the answer's otherwise. */\n headerAlign?: 'start' | 'center'\n siteName: string\n homeId: string | null\n pages: readonly AiLayoutPage[]\n targets: AiLayoutTargets\n extend: (tree: AiValidatedTree) => AiDoctrineViolation[]\n /** The site's design (`aiLayoutDesignOf`), whose pages' closing band the footer sits under. */\n design?: AiLayoutDesign | null\n /** A store's own pages, which the header and footer link by path (AGL-3676). */\n storeLinks?: AiStoreFrameLinks | null\n /** The site's language, which the frame's own words are written in (AGL-3760). */\n language?: string\n /** The header's brand as a site brought in draws it (AGL-4084). */\n brand?: AiLayoutFrameBrand | null\n /** A site brought in's own header and footer (AGL-4108), drawn in place of the design's. */\n source?: AiLayoutFrameSource | null\n}\n\n/** The check a frame answer is held to: read, compiled, stored and checked as a layout, with the layout door's own checks. */\nexport function aiLayoutFrameCheck(input: AiLayoutFrameInput): AiGenerationCheck<AiLayoutFrameBuilt> {\n let answers = 0\n return (answer) => {\n // The last answer a generation takes has its gaps taken out rather than asked about again.\n answers += 1\n return aiLayoutFrameBuild(aiReadLayoutFrame(answer), input, answers >= AI_GENERATION_MAX_ATTEMPTS)\n }\n}\n\n/**\n * A site brought in's header and footer as its source builds them\n * (AGL-4108), with no model: compiled, stored and checked as a layout, held\n * to every check an answer is. `value` is `null` with the violations where it\n * does not pass, and the frame is then designed by the model instead.\n */\nexport function aiLayoutSourceFrameBuild(input: AiLayoutFrameInput & { source: AiLayoutFrameSource }): ReturnType<AiGenerationCheck<AiLayoutFrameBuilt>> {\n const band = input.source.header === 'brand' || input.source.header === 'dark' ? input.source.header : 'plain'\n return aiLayoutFrameBuild({ header: { band, align: 'start', blocks: [] }, footer: null }, input, true)\n}\n\n/** A frame compiled, stored and checked as a layout; `last` takes its gaps out rather than asking about them. */\nfunction aiLayoutFrameBuild(frame: { header: AiLayoutSection | null; footer: AiLayoutSection | null }, input: AiLayoutFrameInput, last: boolean): ReturnType<AiGenerationCheck<AiLayoutFrameBuilt>> {\n if (input.headerAlign && frame.header && !input.source) frame.header.align = input.headerAlign\n const compiled = aiCompileLayoutFrame(\n frame,\n {\n siteName: input.siteName,\n homeId: input.homeId,\n navPages: input.pages,\n closesDark: input.design ? aiLayoutDesignChoices(input.design).coverClose : false,\n // A site that sells carries its cart in the header (AGL-3676).\n cart: (input.targets.listings ?? []).some((listing) => listing.kind === 'products' && listing.cart !== false),\n // And a store's header is a storefront's: no button repeating its navigation, an announcement bar (AGL-3676).\n shopId: aiLayoutShopPageId(input.targets),\n // And its account and policies, written after it (AGL-3676).\n storeLinks: input.storeLinks ?? null,\n ...(input.language ? { language: input.language } : {}),\n ...(input.brand ? { brand: input.brand } : {}),\n ...(input.source ? { source: input.source } : {}),\n },\n input.targets,\n )\n // The pages a source's menu and footer link by id (AGL-4108).\n const sourcePages = input.source ? aiLayoutFrameSourcePageIds(input.source) : []\n const context = {\n screenIds: [\n ...new Set([\n ...input.targets.pages.map((page) => page.id),\n ...input.pages.map((page) => page.id),\n ...(input.homeId ? [input.homeId] : []),\n ...sourcePages,\n ]),\n ],\n formIds: input.targets.forms.map((form) => form.id),\n homeScreenIds: input.targets.homeIds,\n // A link home under the home page's own title says home, in any language (AGL-3760).\n homeLabels: [\n ...input.pages.filter((page) => aiLayoutIsHomeSlug(page.slug)),\n ...input.targets.pages.filter((page) => input.targets.homeIds.includes(page.id)),\n ].map((page) => page.label),\n // A footer's \"Get in touch\" goes to the form, wherever it is placed (AGL-3729).\n ...(input.targets.formPageId ? { formScreenIds: [input.targets.formPageId] } : {}),\n codeBuilt: true,\n // A source's header and footer are its owner's own words and menu\n // (AGL-4108), drawn by code where they repeat: no model wrote a block\n // twice that a component should carry once.\n ...(input.source ? { ownWords: true, reusableComponents: false } : {}),\n }\n const stored = aiLayoutStoredTree(compiled.tree, 'layout', context)\n if (stored.ok === false) {\n console.error('ai layout frame: a compiled layout did not store', {\n error: stored.error,\n })\n return {\n value: null,\n violations: [\n {\n rule: null,\n code: 'layout-compile',\n message: 'This layout could not be built. Try again.',\n detail: stored.error,\n },\n ],\n }\n }\n // No gap and no internal reference in the words of a frame every published page shows.\n const navPages = [...input.targets.pages, ...input.pages]\n const copy = aiLayoutCopyCheck(\n stored.nodes,\n stored.rootId,\n {\n pages: navPages,\n ids: [...navPages.map((page) => page.id), ...input.targets.forms.map((form) => form.id)],\n },\n last,\n )\n const { dropped } = copy\n // An attribute a rule has one answer for is settled, not asked of the model again (AGL-3660).\n const nodes = aiSettleUnrelatedScreenLinks(\n aiSettleMechanicalRules(\n { rootId: stored.rootId, nodes: copy.nodes as unknown as Record<string, AiDoctrineNode> },\n 'layout',\n // And on the last answer, a link that goes nowhere, as the check below reads it (AGL-4076).\n last ? context : undefined,\n ),\n 'layout',\n context,\n ).nodes as unknown as typeof copy.nodes\n // The copy check can take words out of the footer: no heading or column\n // is left with nothing under it (AGL-3735), and none may remain.\n aiLayoutRepairFrameHollows(nodes as unknown as Record<string, unknown>)\n const hollow = aiLayoutFrameHollows(nodes as unknown as Record<string, unknown>)\n const report = validateAiDoctrineTree(\n { rootId: stored.rootId, nodes },\n 'layout',\n context,\n )\n const violations = [\n ...report.violations,\n ...(aiLayoutFrameIsHollow(hollow)\n ? [\n {\n rule: null,\n code: 'layout-frame-hollow',\n message: 'Every footer heading needs something under it, and every footer column more than a heading.',\n detail: aiLayoutFrameHollowsSaid(hollow).join('; '),\n },\n ]\n : []),\n ...copy.violations,\n ...input.extend({\n rootId: stored.rootId,\n nodes,\n repairs: [],\n sourceIds: {},\n score: report.score as AiValidatedTree['score'],\n load: report.load,\n }),\n ]\n return {\n value: violations.length\n ? null\n : { rootId: stored.rootId, nodes, load: report.load, dropped },\n violations,\n }\n}\n\n/** What a frame's links resolve against: the site's pages and its form, as a page's do. */\nexport function aiLayoutFrameTargets(\n job: Pick<AiJob, 'brief' | 'inputs'>,\n inventory: AiSiteInventory | null,\n): AiLayoutTargets {\n return aiLayoutPageTargets({ job, inventory, own: [] })\n}\n"],"names":["aiLayoutDesignChoices","aiCompileLayoutFrame","aiLayoutFrameSourceOf","aiLayoutFrameSourcePageIds","aiLayoutFrameHollows","aiLayoutFrameHollowsSaid","aiLayoutFrameIsHollow","aiLayoutRepairFrameHollows","AI_LAYOUT_LANGUAGE_TEXT","aiReadLayoutFrame","aiLayoutCopyCheck","aiLayoutStoredTree","AI_GENERATION_MAX_ATTEMPTS","aiLayoutSpeaksOtherThanEnglish","aiHomeScreenIds","AI_STEP_TIERS","aiSettleMechanicalRules","aiSettleUnrelatedScreenLinks","validateAiDoctrineTree","aiSiteKindDesignLines","aiJobStepBudget","fenceUntrustedText","UNTRUSTED_TEXT_RULE","aiJobBriefLine","AI_LAYOUT_SITE_PAGES_MAX","aiLayoutIsHomeSlug","aiLayoutSitePages","AI_LAYOUT_FRAME_THINKING_TOKENS","aiLayoutPageTargets","AI_SITE_NAME_TOKEN","AI_JOB_LAYOUT_LANGUAGE_TOKENS","AI_JOB_LAYOUT_LANGUAGE_BUDGET","tier","maxTokens","AI_LAYOUT_FRAME_KIND","AI_JOB_LAYOUT_LANGUAGE_INSTRUCTIONS","text","join","cacheBreakpoint","AI_LAYOUT_SITE_BRAND_INPUT","AI_LAYOUT_SITE_FACTS_INPUT","AI_LAYOUT_SITE_FRAME_INPUT","aiLayoutSiteFrameOf","inputs","AI_LAYOUT_SITE_FACTS_MAX_CHARS","aiLayoutSiteBrandOf","raw","value","logo","src","trim","alt","replace","slice","aspect","Number","isFinite","Math","min","name","aiLayoutSiteFactsOf","lines","Array","isArray","filter","line","description","length","url","aiLayoutSiteFactsText","facts","Boolean","aiLayoutSiteFactsLines","maxChars","aiJobLayoutLanguageInstructions","job","aiLayoutSiteName","given","aiLayoutNavPages","inventory","planned","own","screens","row","template","map","id","label","slug","siteHome","some","page","pages","home","find","plan","language","aiLayoutHomeId","aiLayoutFramePrompt","input","targets","siteName","forms","formPageId","aiLayoutShopPageId","$id","products","listings","listing","kind","placements","placement","role","screenId","aiLayoutFrameCheck","answers","answer","aiLayoutFrameBuild","aiLayoutSourceFrameBuild","band","source","header","align","blocks","footer","frame","last","headerAlign","compiled","homeId","navPages","closesDark","design","coverClose","cart","shopId","storeLinks","brand","sourcePages","context","screenIds","Set","formIds","form","homeScreenIds","homeIds","homeLabels","includes","formScreenIds","codeBuilt","ownWords","reusableComponents","stored","tree","ok","console","error","violations","rule","code","message","detail","copy","nodes","rootId","ids","dropped","undefined","hollow","report","extend","repairs","sourceIds","score","load","aiLayoutFrameTargets"],"mappings":"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"}
@@ -34,7 +34,7 @@ import { aiLayoutSitePages, aiLayoutSitePagesLines, aiLayoutWithSitePages } from
34
34
  import { aiLayoutInventedContactViolations, aiLayoutWithSiteName } from "./ai-layout-site-facts.js";
35
35
  import { aiLayoutWithFullHeight } from "./ai-layout-full-height.js";
36
36
  import { AI_LAYOUT_FRAME_TOOL } from "../layout-language/ai-layout-language.js";
37
- import { AI_JOB_LAYOUT_LANGUAGE_BUDGET, AI_LAYOUT_FRAME_KIND, aiJobLayoutLanguageInstructions, aiLayoutFrameCheck, aiLayoutFramePrompt, aiLayoutFrameTargets, aiLayoutHomeId, aiLayoutNavPages, aiLayoutSiteBrandOf, aiLayoutSiteFactsText, aiLayoutSiteName } from "./ai-job-layout-language.js";
37
+ import { AI_JOB_LAYOUT_LANGUAGE_BUDGET, AI_LAYOUT_FRAME_KIND, aiJobLayoutLanguageInstructions, aiLayoutFrameCheck, aiLayoutFramePrompt, aiLayoutFrameTargets, aiLayoutHomeId, aiLayoutNavPages, aiLayoutSiteBrandOf, aiLayoutSiteFactsText, aiLayoutSiteFrameOf, aiLayoutSiteName, aiLayoutSourceFrameBuild } from "./ai-job-layout-language.js";
38
38
  import { AI_LAYOUT_LANGUAGE_THINKING, aiJobUsesLayoutLanguage, aiLayoutDesignOf } from "./ai-job-page-language.js";
39
39
  import { aiJobStepBudget } from "./ai-job-budget.js";
40
40
  import { registerAiJobStep } from "./ai-jobs.js";
@@ -342,6 +342,60 @@ export function createAiJobLayoutStep(deps = {}) {
342
342
  const siteName = aiLayoutSiteName(job);
343
343
  const pages = aiLayoutNavPages(job, inventory);
344
344
  const targets = aiLayoutFrameTargets(job, inventory);
345
+ const frameInput = _extends({}, aiSiteStyleTokens(job.inputs), {
346
+ siteName,
347
+ homeId: aiLayoutHomeId(pages, inventory),
348
+ pages,
349
+ targets,
350
+ // A site brought in may state what its own header and footer state (AGL-4084).
351
+ extend: aiLayoutChecks(inventory, plan, [
352
+ job.brief,
353
+ aiLayoutSiteFactsText(job.inputs)
354
+ ].filter(Boolean).join('\n')),
355
+ design: aiLayoutDesignOf(job, '/'),
356
+ brand: aiLayoutSiteBrandOf(job.inputs),
357
+ storeLinks: aiStoreFrameLinksOf(job.inputs)
358
+ }, ((_job_plan = job.plan) == null ? void 0 : _job_plan.language) ? {
359
+ language: job.plan.language
360
+ } : {});
361
+ // A site brought in whose menu the reader found (AGL-4108): its own
362
+ // header and footer, drawn by code, and no model asked. One that does
363
+ // not pass the layout's checks is designed by the model instead.
364
+ const source = aiLayoutSiteFrameOf(job.inputs);
365
+ if (source) {
366
+ const drawn = aiLayoutSourceFrameBuild(_extends({}, frameInput, {
367
+ source
368
+ }));
369
+ if (drawn.value) {
370
+ throwIfAiJobCanceled(signal);
371
+ const draft = await writeAiDraft(firestore, {
372
+ kind: 'layout',
373
+ hostId,
374
+ id: draftId,
375
+ uid: job.createdBy,
376
+ org,
377
+ name,
378
+ nodes: drawn.value.nodes,
379
+ aiJobId: aiOriginJobId(job),
380
+ now
381
+ });
382
+ if (draft.ok === false) {
383
+ if (draft.status === 404) throw new Error(`site ${hostId} vanished while its layout was built`);
384
+ return aiUnspentOutcome(model, {
385
+ review: aiLimitReview(draft.error)
386
+ });
387
+ }
388
+ return aiUnspentOutcome(model, {
389
+ outputs: [
390
+ output(draft, drawn.value.load)
391
+ ]
392
+ });
393
+ }
394
+ console.error('ai layout: a source frame did not pass its checks; designing it instead', {
395
+ jobId: job.$id,
396
+ violations: drawn.violations.map((violation)=>violation.code)
397
+ });
398
+ }
345
399
  const result = await runValidatedGeneration(AI_LAYOUT_FRAME_KIND, _extends({
346
400
  step: 'job.layout',
347
401
  model,
@@ -361,22 +415,7 @@ export function createAiJobLayoutStep(deps = {}) {
361
415
  tool: AI_LAYOUT_FRAME_TOOL,
362
416
  maxTokens: AI_JOB_LAYOUT_LANGUAGE_BUDGET.maxTokens(model)
363
417
  }, AI_LAYOUT_LANGUAGE_THINKING, {
364
- check: aiLayoutFrameCheck(_extends({}, aiSiteStyleTokens(job.inputs), {
365
- siteName,
366
- homeId: aiLayoutHomeId(pages, inventory),
367
- pages,
368
- targets,
369
- // A site brought in may state what its own header and footer state (AGL-4084).
370
- extend: aiLayoutChecks(inventory, plan, [
371
- job.brief,
372
- aiLayoutSiteFactsText(job.inputs)
373
- ].filter(Boolean).join('\n')),
374
- design: aiLayoutDesignOf(job, '/'),
375
- brand: aiLayoutSiteBrandOf(job.inputs),
376
- storeLinks: aiStoreFrameLinksOf(job.inputs)
377
- }, ((_job_plan = job.plan) == null ? void 0 : _job_plan.language) ? {
378
- language: job.plan.language
379
- } : {}))
418
+ check: aiLayoutFrameCheck(frameInput)
380
419
  }, signal ? {
381
420
  signal
382
421
  } : {}));