@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.246

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 (116) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +3 -1
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +7 -0
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-product-zones.d.ts +3 -1
  7. package/src/lib/components/ai-product-zones.js.map +1 -1
  8. package/src/lib/components/ai-products-hub-card.component.js +1 -1
  9. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  10. package/src/lib/components/ai-products-proposals.component.js +17 -9
  11. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  12. package/src/lib/components/ai-site-start-card.component.js +6 -4
  13. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  14. package/src/lib/components/ai-upsell-dialog.component.js +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  16. package/src/lib/jobs/ai-build-capabilities.js +162 -2
  17. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  18. package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
  19. package/src/lib/jobs/ai-job-build-step.js +85 -16
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
  22. package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
  23. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
  24. package/src/lib/jobs/ai-job-layout-language.js +13 -0
  25. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  26. package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
  27. package/src/lib/jobs/ai-job-navigation-step.js +439 -0
  28. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
  29. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  30. package/src/lib/jobs/ai-job-page-language.js +21 -5
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-page-sections.js +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  35. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  38. package/src/lib/jobs/ai-job-products-step.js +2 -2
  39. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  41. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  42. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  43. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  44. package/src/lib/jobs/ai-job-site-content.js +28 -12
  45. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  46. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  47. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  48. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  49. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  50. package/src/lib/jobs/ai-job-site-step.js +70 -10
  51. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  52. package/src/lib/jobs/ai-site-publish.d.ts +62 -4
  53. package/src/lib/jobs/ai-site-publish.js +203 -56
  54. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  56. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  57. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  58. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  59. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  60. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  61. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  62. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  63. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  68. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  69. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  70. package/src/lib/model/ai-build-job.d.ts +23 -0
  71. package/src/lib/model/ai-build-job.js +102 -13
  72. package/src/lib/model/ai-build-job.js.map +1 -1
  73. package/src/lib/model/ai-build-plan.d.ts +25 -0
  74. package/src/lib/model/ai-build-plan.js +45 -1
  75. package/src/lib/model/ai-build-plan.js.map +1 -1
  76. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  77. package/src/lib/model/ai-credit-estimate.js +51 -0
  78. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  79. package/src/lib/model/ai-job-activity.js +4 -1
  80. package/src/lib/model/ai-job-activity.js.map +1 -1
  81. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  82. package/src/lib/model/ai-jobs.types.js +4 -1
  83. package/src/lib/model/ai-jobs.types.js.map +1 -1
  84. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  85. package/src/lib/model/ai-layout-menus.js +518 -0
  86. package/src/lib/model/ai-layout-menus.js.map +1 -0
  87. package/src/lib/model/ai-products.d.ts +31 -1
  88. package/src/lib/model/ai-products.js +34 -1
  89. package/src/lib/model/ai-products.js.map +1 -1
  90. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  91. package/src/lib/model/ai-site-inventory.js.map +1 -1
  92. package/src/lib/model/ai-site-job.d.ts +10 -0
  93. package/src/lib/model/ai-site-job.js +36 -9
  94. package/src/lib/model/ai-site-job.js.map +1 -1
  95. package/src/lib/model/ai-site-store-pages.js +1 -1
  96. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  97. package/src/lib/runtime/ai-doctrine-validators.d.ts +53 -0
  98. package/src/lib/runtime/ai-doctrine-validators.js +166 -25
  99. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  100. package/src/lib/runtime/ai-doctrine.js +7 -2
  101. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  102. package/src/lib/runtime/ai-palette.generated.js +1 -1
  103. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  104. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  105. package/src/lib/runtime/ai-products-generation.js +12 -6
  106. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  107. package/src/lib/runtime/site-inventory.d.ts +12 -0
  108. package/src/lib/runtime/site-inventory.js +29 -1
  109. package/src/lib/runtime/site-inventory.js.map +1 -1
  110. package/src/lib/server.js +12 -0
  111. package/src/lib/server.js.map +1 -1
  112. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  113. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  114. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  115. package/src/lib/tools/ai-products-tool.js +15 -5
  116. package/src/lib/tools/ai-products-tool.js.map +1 -1
@@ -73,6 +73,22 @@ export interface AiSitePublishDeps {
73
73
  * door). Every other build leaves drafts.
74
74
  */
75
75
  export declare function aiJobPublishesBuild(job: Pick<AiJob, 'kind' | 'inputs'>): boolean;
76
+ /**
77
+ * The host fields a guided start writes for its plan's language (AGL-3750),
78
+ * as the Languages card saves one language: `locales` of that one and
79
+ * `defaultLocale` the same. `null` — nothing written — where the plan said no
80
+ * valid language, where it is the language the site already publishes in
81
+ * (English for a new site), and on a site with more than one language, whose
82
+ * languages are the owner's decision. Nothing is translated: the pages were
83
+ * written in the brief's language, and this says so to the reader.
84
+ */
85
+ export declare function aiSiteLanguageUpdates(host: {
86
+ locales?: unknown;
87
+ defaultLocale?: unknown;
88
+ }, language: string | null | undefined): {
89
+ locales: string[];
90
+ defaultLocale: string;
91
+ } | null;
76
92
  /** Whether a finished job of this shape publishes what it built. */
77
93
  export declare function aiJobPublishesSite(job: Pick<AiJob, 'kind' | 'inputs'>): boolean;
78
94
  /**
@@ -103,10 +119,12 @@ interface NavEntry {
103
119
  export declare function aiSiteUnwrittenBlogHrefs(blogUnwritten: boolean | undefined): string[];
104
120
  /**
105
121
  * The layout's nodes with its links to the retired starter home pointed at
106
- * the new home, and a link for each proposed entry the header does not
107
- * already carry, placed in the header's toolbar after its existing links.
108
- * `null` when nothing changes, or when the layout has no toolbar to hold an
109
- * entry and no link to repoint.
122
+ * the new home, its links to pages the start did not write taken out, and a
123
+ * link for each proposed entry the header does not already carry, added by
124
+ * the navigation op's `aiLayoutWithMenuChanges` (AGL-3747): to the header's
125
+ * menu and its phone menu, after their items, or after the brand in a
126
+ * starter's Toolbar. `null` when nothing changes, or when the layout has no
127
+ * header to hold an entry and no link to repoint or take out.
110
128
  */
111
129
  export declare function aiLayoutWithNavigation(nodes: NodesMap, input: {
112
130
  entries: readonly NavEntry[];
@@ -115,6 +133,41 @@ export declare function aiLayoutWithNavigation(nodes: NodesMap, input: {
115
133
  /** Paths whose links come out: a blog the start linked and never wrote (AGL-3660). */
116
134
  droppedHrefs?: readonly string[];
117
135
  }): NodesMap | null;
136
+ /** The blog a start writes, as its header entry links it (`aiSiteBlogNavPage`). */
137
+ export interface AiSiteBlogEntry {
138
+ label: string;
139
+ href: string;
140
+ }
141
+ /**
142
+ * The layout's nodes with the blog's header entry put back (AGL-3747): a
143
+ * start whose posts part failed published with every link to the blog taken
144
+ * out (AGL-3660), and Try again's posts went live with none to reach them.
145
+ * Added by the navigation op's `aiLayoutWithMenuChanges`, as the start's
146
+ * other entries are — the bar's menu and the phone menu — second after the
147
+ * home page's item, as the start first linked it (AGL-3676), else last.
148
+ * `null` where some link already opens the blog, or the layout has no header
149
+ * menu to hold it.
150
+ */
151
+ export declare function aiLayoutWithBlogEntry(nodes: NodesMap, input: {
152
+ blog: AiSiteBlogEntry;
153
+ homeId?: string | null;
154
+ }): NodesMap | null;
155
+ /**
156
+ * Puts the blog's header entry back on a live start whose posts Try again
157
+ * wrote (AGL-3747), as a new live version of the layout its published pages
158
+ * render inside, in one batch with the publish outbox entry that drops the
159
+ * live site's cached pages, the drop itself attempted at once. Returns
160
+ * whether the entry went live; never throws for a layout it cannot change.
161
+ */
162
+ export declare function aiRestoreGuidedBlogEntry(firestore: Firestore, input: {
163
+ job: Pick<AiJob, '$id' | 'hostId'>;
164
+ /** The start's published pages: the layout they share is the one changed. */
165
+ pageIds: readonly string[];
166
+ /** The published home page, which the blog's entry follows. */
167
+ homeId?: string | null;
168
+ blog: AiSiteBlogEntry;
169
+ now: Date;
170
+ }, deps?: Pick<AiSitePublishDeps, 'dropCache'>): Promise<boolean>;
118
171
  /**
119
172
  * A retry's publish merged into the start's first (AGL-3730): the pages it
120
173
  * put live join the published list, a page it published leaves the drafts,
@@ -144,5 +197,10 @@ export declare function aiPublishGuidedSite(firestore: Firestore, input: {
144
197
  * starter still holds stays a draft and says so.
145
198
  */
146
199
  retry?: boolean;
200
+ /**
201
+ * The language the plan says the brief and copy are in (AGL-3750): the
202
+ * site publishes in it, where `aiSiteLanguageUpdates` says so.
203
+ */
204
+ language?: string | null;
147
205
  }, deps?: AiSitePublishDeps): Promise<AiJobSitePublish>;
148
206
  export {};
@@ -18,12 +18,16 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
19
19
  import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
20
20
  import { blockingRouteOwner, normalizeScreenSlug, SCREEN_ROOT_PATH, screenRoutePathToUrl } from "@aglyn/aglyn/app-utils/screen-route";
21
+ import { resolvePageLocale } from "@aglyn/aglyn/app-utils/seo-locale";
21
22
  import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
22
23
  import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
23
24
  import { sendGa4SitePublished } from "@aglyn/tenant-data-admin/server/ga4-measurement-protocol";
24
25
  import { FieldValue } from "firebase-admin/firestore";
26
+ import { aiLayoutFrameHollows, aiLayoutFrameHollowsSaid, aiLayoutFrameIsHollow, aiLayoutRepairFrameHollows, aiLayoutWithoutFrameHollows } from "../layout-language/ai-layout-frame-hollows.js";
27
+ import { aiPlanLanguage } from "../model/ai-build-plan.js";
25
28
  import { AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
26
29
  import { aiLayoutWithStoreLinks } from "../model/ai-layout-store-links.js";
30
+ import { aiLayoutWithMenuChanges, aiNoMenuChanges } from "../model/ai-layout-menus.js";
27
31
  import { AI_STORE_LINKS_SOURCE_FIELD as STORE_LINKS_SOURCE_FIELD } from "./ai-job-store-links-layout.js";
28
32
  /**
29
33
  * Whether a build puts its pages live (AGL-3616): only where its request
@@ -34,6 +38,34 @@ import { AI_STORE_LINKS_SOURCE_FIELD as STORE_LINKS_SOURCE_FIELD } from "./ai-jo
34
38
  var _job_inputs, _job_inputs1;
35
39
  return job.kind === 'build' && ((_job_inputs = job.inputs) == null ? void 0 : _job_inputs['publish']) === true && ((_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['publishConfirmed']) === true;
36
40
  }
41
+ /**
42
+ * The host fields a guided start writes for its plan's language (AGL-3750),
43
+ * as the Languages card saves one language: `locales` of that one and
44
+ * `defaultLocale` the same. `null` — nothing written — where the plan said no
45
+ * valid language, where it is the language the site already publishes in
46
+ * (English for a new site), and on a site with more than one language, whose
47
+ * languages are the owner's decision. Nothing is translated: the pages were
48
+ * written in the brief's language, and this says so to the reader.
49
+ */ export function aiSiteLanguageUpdates(host, language) {
50
+ const code = aiPlanLanguage(language);
51
+ if (!code) return null;
52
+ const locales = Array.isArray(host.locales) ? host.locales.filter((one)=>typeof one === 'string' && one.trim() !== '') : [];
53
+ if (locales.length > 1) return null;
54
+ const current = resolvePageLocale({
55
+ host: _extends({}, typeof host.defaultLocale === 'string' ? {
56
+ defaultLocale: host.defaultLocale
57
+ } : {}, {
58
+ locales
59
+ })
60
+ });
61
+ if (current === code) return null;
62
+ return {
63
+ locales: [
64
+ code
65
+ ],
66
+ defaultLocale: code
67
+ };
68
+ }
37
69
  /** Whether a finished job of this shape publishes what it built. */ export function aiJobPublishesSite(job) {
38
70
  var _job_inputs;
39
71
  return job.kind === 'site' && ((_job_inputs = job.inputs) == null ? void 0 : _job_inputs['autoConfirm']) === true;
@@ -80,10 +112,12 @@ function navEntriesOf(outputs) {
80
112
  }
81
113
  /**
82
114
  * The layout's nodes with its links to the retired starter home pointed at
83
- * the new home, and a link for each proposed entry the header does not
84
- * already carry, placed in the header's toolbar after its existing links.
85
- * `null` when nothing changes, or when the layout has no toolbar to hold an
86
- * entry and no link to repoint.
115
+ * the new home, its links to pages the start did not write taken out, and a
116
+ * link for each proposed entry the header does not already carry, added by
117
+ * the navigation op's `aiLayoutWithMenuChanges` (AGL-3747): to the header's
118
+ * menu and its phone menu, after their items, or after the brand in a
119
+ * starter's Toolbar. `null` when nothing changes, or when the layout has no
120
+ * header to hold an entry and no link to repoint or take out.
87
121
  */ export function aiLayoutWithNavigation(nodes, input) {
88
122
  var _input_droppedHrefs;
89
123
  const map = {};
@@ -113,6 +147,9 @@ function navEntriesOf(outputs) {
113
147
  var _node_nodes;
114
148
  if ((_node_nodes = node.nodes) == null ? void 0 : _node_nodes.some((child)=>gone.has(child))) node.nodes = node.nodes.filter((child)=>!gone.has(child));
115
149
  }
150
+ // A footer heading whose links all went goes with them, and its column
151
+ // when that leaves it only the heading (AGL-3735).
152
+ aiLayoutRepairFrameHollows(map);
116
153
  changed = true;
117
154
  }
118
155
  }
@@ -128,51 +165,140 @@ function navEntriesOf(outputs) {
128
165
  }
129
166
  }
130
167
  }
168
+ // The entries go into the header through the navigation op's own code
169
+ // (AGL-3747): the bar's menu and the phone menu's, after the items already
170
+ // there, each looking like its neighbours. A page some link already opens,
171
+ // or an item of the same name, is not added again.
131
172
  const linked = new Set(links.map((link)=>{
132
173
  var _link_props;
133
174
  return (_link_props = link.props) == null ? void 0 : _link_props['screenId'];
134
175
  }).filter(Boolean));
135
- const toolbar = Object.values(map).find((node)=>node.componentId === 'muiToolbar');
136
176
  const missing = input.entries.filter((entry)=>!linked.has(entry.screenId));
137
- if (toolbar && missing.length) {
138
- var _toolbar_nodes;
139
- const children = (_toolbar_nodes = toolbar.nodes) != null ? _toolbar_nodes : [];
140
- // After the last link already in the toolbar (the brand), before the
141
- // search, the mode switch and the call to action.
142
- const lastLink = children.reduce((at, id, index)=>{
143
- var _map_id;
144
- return ((_map_id = map[id]) == null ? void 0 : _map_id.componentId) === 'muiScreenLink' ? index : at;
145
- }, -1);
146
- const added = missing.map((entry)=>{
147
- const id = `ai_nav_${createResourceUid()}`;
148
- map[id] = {
149
- $id: id,
150
- componentId: 'muiScreenLink',
151
- pluginId: 'mui',
152
- parentId: toolbar.$id,
153
- props: {
154
- children: entry.label,
155
- screenId: entry.screenId,
156
- renderAs: 'link',
157
- color: 'inherit'
158
- },
159
- sx: {
160
- textDecoration: 'none',
161
- whiteSpace: 'nowrap'
162
- },
163
- nodes: []
164
- };
165
- return id;
177
+ if (missing.length) {
178
+ const added = aiLayoutWithMenuChanges(map, [
179
+ 'header'
180
+ ], _extends({}, aiNoMenuChanges(), {
181
+ add: missing.map((entry)=>({
182
+ parent: null,
183
+ label: entry.label,
184
+ to: {
185
+ screenId: entry.screenId
186
+ }
187
+ }))
188
+ }), {
189
+ newId: ()=>`ai_nav_${createResourceUid()}`
166
190
  });
167
- toolbar.nodes = [
168
- ...children.slice(0, lastLink + 1),
169
- ...added,
170
- ...children.slice(lastLink + 1)
171
- ];
172
- changed = true;
191
+ if (added.nodes) return added.nodes;
173
192
  }
174
193
  return changed ? map : null;
175
194
  }
195
+ /**
196
+ * The layout's nodes with the blog's header entry put back (AGL-3747): a
197
+ * start whose posts part failed published with every link to the blog taken
198
+ * out (AGL-3660), and Try again's posts went live with none to reach them.
199
+ * Added by the navigation op's `aiLayoutWithMenuChanges`, as the start's
200
+ * other entries are — the bar's menu and the phone menu — second after the
201
+ * home page's item, as the start first linked it (AGL-3676), else last.
202
+ * `null` where some link already opens the blog, or the layout has no header
203
+ * menu to hold it.
204
+ */ export function aiLayoutWithBlogEntry(nodes, input) {
205
+ var _added_nodes;
206
+ const blogHrefs = new Set([
207
+ input.blog.href,
208
+ ...AI_SITE_BLOG_SLUGS.map((slug)=>`/${slug}`)
209
+ ]);
210
+ const linked = Object.values(nodes).some((node)=>{
211
+ var _ref;
212
+ var _node_props;
213
+ return (node == null ? void 0 : node.componentId) === 'muiScreenLink' && blogHrefs.has(String((_ref = (_node_props = node.props) == null ? void 0 : _node_props['href']) != null ? _ref : ''));
214
+ });
215
+ if (linked) return null;
216
+ const home = [
217
+ ...input.homeId ? [
218
+ {
219
+ screenId: input.homeId
220
+ }
221
+ ] : [],
222
+ {
223
+ href: '/'
224
+ }
225
+ ];
226
+ const added = aiLayoutWithMenuChanges(nodes, [
227
+ 'header'
228
+ ], _extends({}, aiNoMenuChanges(), {
229
+ add: [
230
+ {
231
+ parent: null,
232
+ label: input.blog.label,
233
+ to: {
234
+ href: input.blog.href
235
+ },
236
+ after: home
237
+ }
238
+ ]
239
+ }), {
240
+ newId: ()=>`ai_nav_${createResourceUid()}`
241
+ });
242
+ return (_added_nodes = added.nodes) != null ? _added_nodes : null;
243
+ }
244
+ /**
245
+ * Puts the blog's header entry back on a live start whose posts Try again
246
+ * wrote (AGL-3747), as a new live version of the layout its published pages
247
+ * render inside, in one batch with the publish outbox entry that drops the
248
+ * live site's cached pages, the drop itself attempted at once. Returns
249
+ * whether the entry went live; never throws for a layout it cannot change.
250
+ */ export async function aiRestoreGuidedBlogEntry(firestore, input, deps = {}) {
251
+ var _deps_dropCache, _input_job_hostId;
252
+ const dropCache = (_deps_dropCache = deps.dropCache) != null ? _deps_dropCache : dropPluginSiteCache;
253
+ const hostId = (_input_job_hostId = input.job.hostId) != null ? _input_job_hostId : '';
254
+ if (!hostId || !input.pageIds.length) return false;
255
+ const layoutId = await aiPagesLayoutId(firestore, hostId, input.pageIds);
256
+ if (!layoutId) return false;
257
+ const write = await aiLayoutVersionWrite(firestore, {
258
+ hostId,
259
+ layoutId,
260
+ transform: (nodes)=>{
261
+ var _input_homeId;
262
+ return aiLayoutWithBlogEntry(nodes, {
263
+ blog: input.blog,
264
+ homeId: (_input_homeId = input.homeId) != null ? _input_homeId : null
265
+ });
266
+ },
267
+ storeLinks: null,
268
+ jobId: input.job.$id,
269
+ now: input.now,
270
+ displayName: 'Navigation for the blog'
271
+ });
272
+ if (!(write == null ? void 0 : write.version)) return false;
273
+ const batch = firestore.batch();
274
+ batch.create(write.versionRef, write.version);
275
+ batch.set(write.layoutRef, {
276
+ versionId: write.versionRef.id,
277
+ updatedAt: input.now
278
+ }, {
279
+ merge: true
280
+ });
281
+ // A layout change reaches every page, so the announce asks for the whole site.
282
+ const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid());
283
+ batch.set(outboxRef, {
284
+ hostId,
285
+ paths: [
286
+ SCREEN_ROOT_PATH
287
+ ],
288
+ createdAt: FieldValue.serverTimestamp(),
289
+ attempts: 0,
290
+ entireHost: true
291
+ });
292
+ await batch.commit();
293
+ const dropped = await dropCache({
294
+ hostIds: [
295
+ hostId
296
+ ],
297
+ reason: 'guided AI site start linked its blog'
298
+ });
299
+ if (dropped.complete) await outboxRef.delete().catch(()=>undefined);
300
+ return true;
301
+ }
176
302
  /**
177
303
  * A retry's publish merged into the start's first (AGL-3730): the pages it
178
304
  * put live join the published list, a page it published leaves the drafts,
@@ -342,6 +468,14 @@ function navEntriesOf(outputs) {
342
468
  } else if (placeholder && entries[placeholder]) {
343
469
  hostUpdates['defaultHomeScreenId'] = FieldValue.delete();
344
470
  }
471
+ // The site's language, from its plan (AGL-3750): every page's `<html lang>`
472
+ // reads it, so the announce below asks for the whole site.
473
+ const language = aiSiteLanguageUpdates({
474
+ locales: host.get('locales'),
475
+ defaultLocale: host.get('defaultLocale')
476
+ }, input.language);
477
+ if (language) Object.assign(hostUpdates, language);
478
+ const entireHost = layoutWrites.length > 0 || language !== null;
345
479
  batch.update(hostRef, hostUpdates);
346
480
  for (const page of accepted){
347
481
  batch.set(hostRef.collection('screens').doc(page.id), {
@@ -370,7 +504,7 @@ function navEntriesOf(outputs) {
370
504
  ],
371
505
  createdAt: FieldValue.serverTimestamp(),
372
506
  attempts: 0
373
- }, layoutWrites.length ? {
507
+ }, entireHost ? {
374
508
  entireHost: true
375
509
  } : {}));
376
510
  try {
@@ -400,7 +534,7 @@ function navEntriesOf(outputs) {
400
534
  hostId
401
535
  ],
402
536
  reason: 'guided AI site start published'
403
- }, layoutWrites.length ? {} : {
537
+ }, entireHost ? {} : {
404
538
  paths: {
405
539
  [hostId]: paths
406
540
  }
@@ -421,9 +555,21 @@ function navEntriesOf(outputs) {
421
555
  */ async function aiNavigationLayoutWrite(firestore, input) {
422
556
  var _input_storeLinks;
423
557
  if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length && !input.storeLinks) return null;
424
- const hostRef = firestore.collection('hosts').doc(input.hostId);
558
+ const layoutId = await aiPagesLayoutId(firestore, input.hostId, input.pageIds);
559
+ if (!layoutId) return null;
560
+ return aiLayoutVersionWrite(firestore, {
561
+ hostId: input.hostId,
562
+ layoutId,
563
+ transform: (nodes)=>aiLayoutWithNavigation(nodes, input),
564
+ storeLinks: (_input_storeLinks = input.storeLinks) != null ? _input_storeLinks : null,
565
+ jobId: input.jobId,
566
+ now: input.now
567
+ });
568
+ }
569
+ /** The one layout the pages render inside, by their current versions; `null` where they do not share one. */ async function aiPagesLayoutId(firestore, hostId, pageIds) {
570
+ const hostRef = firestore.collection('hosts').doc(hostId);
425
571
  const layoutIds = new Set();
426
- for (const id of input.pageIds){
572
+ for (const id of pageIds){
427
573
  var _ref;
428
574
  const screen = await hostRef.collection('screens').doc(id).get();
429
575
  const versionId = screen.get('versionId');
@@ -431,18 +577,9 @@ function navEntriesOf(outputs) {
431
577
  const layoutId = (_ref = version == null ? void 0 : version.get('layoutId')) != null ? _ref : screen.get('layoutId');
432
578
  if (typeof layoutId === 'string' && layoutId) layoutIds.add(layoutId);
433
579
  }
434
- if (layoutIds.size !== 1) return null;
435
- const [layoutId] = [
580
+ return layoutIds.size === 1 ? [
436
581
  ...layoutIds
437
- ];
438
- return aiLayoutVersionWrite(firestore, {
439
- hostId: input.hostId,
440
- layoutId,
441
- transform: (nodes)=>aiLayoutWithNavigation(nodes, input),
442
- storeLinks: (_input_storeLinks = input.storeLinks) != null ? _input_storeLinks : null,
443
- jobId: input.jobId,
444
- now: input.now
445
- });
582
+ ][0] : null;
446
583
  }
447
584
  /**
448
585
  * The new live version of one layout: `transform` applied to the version
@@ -451,6 +588,7 @@ function navEntriesOf(outputs) {
451
588
  * to the live version otherwise; a draft with nothing more to add becomes
452
589
  * the live version as it is. `null` when nothing changes.
453
590
  */ async function aiLayoutVersionWrite(firestore, input) {
591
+ var _aiLayoutWithoutFrameHollows, _input_displayName;
454
592
  var _input_storeLinks;
455
593
  const { layoutId } = input;
456
594
  const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(layoutId);
@@ -471,6 +609,15 @@ function navEntriesOf(outputs) {
471
609
  const added = aiLayoutWithStoreLinks(next != null ? next : nodes, links.links, links.screenPaths);
472
610
  next = (_added_nodes = added.nodes) != null ? _added_nodes : next;
473
611
  }
612
+ // What a publish puts live has no hollow heading, column or menu (AGL-3735).
613
+ next = (_aiLayoutWithoutFrameHollows = aiLayoutWithoutFrameHollows(next != null ? next : nodes)) != null ? _aiLayoutWithoutFrameHollows : next;
614
+ const hollow = aiLayoutFrameHollows(next != null ? next : nodes);
615
+ if (aiLayoutFrameIsHollow(hollow)) {
616
+ console.error('ai site publish: a layout is still hollow after its repair', {
617
+ layoutId,
618
+ hollow: aiLayoutFrameHollowsSaid(hollow)
619
+ });
620
+ }
474
621
  if (!next) return fromDraft && draftRef ? {
475
622
  layoutRef,
476
623
  versionRef: draftRef,
@@ -485,7 +632,7 @@ function navEntriesOf(outputs) {
485
632
  version: {
486
633
  layoutId,
487
634
  hostId: input.hostId,
488
- displayName: 'Navigation for the new pages',
635
+ displayName: (_input_displayName = input.displayName) != null ? _input_displayName : 'Navigation for the new pages',
489
636
  nodes: Buffer.from(packed),
490
637
  createdAt: input.now,
491
638
  updatedAt: input.now,