@aglyn/plugins-ai 1.0.0-beta.243 → 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 (123) 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 +24 -6
  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 +5 -2
  30. package/src/lib/jobs/ai-job-page-language.js +38 -10
  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-page-step.js +11 -4
  35. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  36. package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
  37. package/src/lib/jobs/ai-job-plan-step.js +16 -7
  38. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  39. package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
  40. package/src/lib/jobs/ai-job-products-step.js +2 -2
  41. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
  43. package/src/lib/jobs/ai-job-settings-step.js +695 -0
  44. package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
  45. package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
  46. package/src/lib/jobs/ai-job-site-content.js +28 -12
  47. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  48. package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
  49. package/src/lib/jobs/ai-job-site-datasets.js +10 -3
  50. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  51. package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
  52. package/src/lib/jobs/ai-job-site-step.js +86 -12
  53. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  54. package/src/lib/jobs/ai-site-publish.d.ts +76 -4
  55. package/src/lib/jobs/ai-site-publish.js +231 -59
  56. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
  58. package/src/lib/layout-language/ai-layout-compiler.js +26 -25
  59. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
  61. package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
  62. package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
  64. package/src/lib/layout-language/ai-layout-frame.js +83 -13
  65. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  66. package/src/lib/layout-language/ai-layout-links.js +5 -2
  67. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +34 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
  72. package/src/lib/layout-language/ai-layout-tree.js +17 -0
  73. package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
  74. package/src/lib/model/ai-build-job.d.ts +23 -0
  75. package/src/lib/model/ai-build-job.js +102 -13
  76. package/src/lib/model/ai-build-job.js.map +1 -1
  77. package/src/lib/model/ai-build-plan.d.ts +25 -0
  78. package/src/lib/model/ai-build-plan.js +45 -1
  79. package/src/lib/model/ai-build-plan.js.map +1 -1
  80. package/src/lib/model/ai-credit-estimate.d.ts +52 -0
  81. package/src/lib/model/ai-credit-estimate.js +51 -0
  82. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  83. package/src/lib/model/ai-job-activity.js +4 -1
  84. package/src/lib/model/ai-job-activity.js.map +1 -1
  85. package/src/lib/model/ai-jobs.types.d.ts +10 -2
  86. package/src/lib/model/ai-jobs.types.js +4 -1
  87. package/src/lib/model/ai-jobs.types.js.map +1 -1
  88. package/src/lib/model/ai-layout-menus.d.ts +104 -0
  89. package/src/lib/model/ai-layout-menus.js +518 -0
  90. package/src/lib/model/ai-layout-menus.js.map +1 -0
  91. package/src/lib/model/ai-products.d.ts +31 -1
  92. package/src/lib/model/ai-products.js +34 -1
  93. package/src/lib/model/ai-products.js.map +1 -1
  94. package/src/lib/model/ai-site-inventory.d.ts +15 -0
  95. package/src/lib/model/ai-site-inventory.js.map +1 -1
  96. package/src/lib/model/ai-site-job.d.ts +10 -0
  97. package/src/lib/model/ai-site-job.js +36 -9
  98. package/src/lib/model/ai-site-job.js.map +1 -1
  99. package/src/lib/model/ai-site-store-pages.js +1 -1
  100. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  101. package/src/lib/runtime/ai-doctrine-validators.d.ts +95 -1
  102. package/src/lib/runtime/ai-doctrine-validators.js +264 -36
  103. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  104. package/src/lib/runtime/ai-doctrine.js +7 -2
  105. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  106. package/src/lib/runtime/ai-palette.generated.js +1 -1
  107. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  108. package/src/lib/runtime/ai-products-generation.d.ts +4 -2
  109. package/src/lib/runtime/ai-products-generation.js +12 -6
  110. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  111. package/src/lib/runtime/site-inventory.d.ts +12 -0
  112. package/src/lib/runtime/site-inventory.js +29 -1
  113. package/src/lib/runtime/site-inventory.js.map +1 -1
  114. package/src/lib/server.js +12 -0
  115. package/src/lib/server.js.map +1 -1
  116. package/src/lib/testing/stand-in-dataset-index.js +37 -3
  117. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
  118. package/src/lib/tools/ai-products-tool.d.ts +3 -1
  119. package/src/lib/tools/ai-products-tool.js +15 -5
  120. package/src/lib/tools/ai-products-tool.js.map +1 -1
  121. package/src/lib/usage/ai-quota-checks.d.ts +13 -3
  122. package/src/lib/usage/ai-quota-checks.js +46 -8
  123. package/src/lib/usage/ai-quota-checks.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,47 @@ 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>;
171
+ /**
172
+ * A retry's publish merged into the start's first (AGL-3730): the pages it
173
+ * put live join the published list, a page it published leaves the drafts,
174
+ * and a page it left a draft is reported with its newest reason.
175
+ */
176
+ export declare function aiMergeSitePublish(previous: AiJobSitePublish, retried: AiJobSitePublish): AiJobSitePublish;
118
177
  /**
119
178
  * Publishes the pages a guided site start built. Never throws for a page; a
120
179
  * Firestore failure of the publish write itself reports every page as a
@@ -130,5 +189,18 @@ export declare function aiPublishGuidedSite(firestore: Firestore, input: {
130
189
  unwrittenHrefs?: readonly string[];
131
190
  /** A store's links a build drafted into the site's own layout (AGL-3676), which go live with its pages. */
132
191
  storeLinks?: AiSitePublishStoreLinks | null;
192
+ /**
193
+ * The start published once already, and Try again rebuilt what failed
194
+ * (AGL-3730): only `outputs` go live, beside what is. The home address is
195
+ * never taken from the starter here, the starter is never retired, and
196
+ * `site_published` is not sent again; a retried page at `/` that the
197
+ * starter still holds stays a draft and says so.
198
+ */
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;
133
205
  }, deps?: AiSitePublishDeps): Promise<AiJobSitePublish>;
134
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,162 @@ 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
+ }
302
+ /**
303
+ * A retry's publish merged into the start's first (AGL-3730): the pages it
304
+ * put live join the published list, a page it published leaves the drafts,
305
+ * and a page it left a draft is reported with its newest reason.
306
+ */ export function aiMergeSitePublish(previous, retried) {
307
+ var _retried_liveUrl;
308
+ const now = new Set([
309
+ ...retried.published,
310
+ ...retried.drafts
311
+ ].map((page)=>page.id));
312
+ return {
313
+ liveUrl: (_retried_liveUrl = retried.liveUrl) != null ? _retried_liveUrl : previous.liveUrl,
314
+ published: [
315
+ ...previous.published.filter((page)=>!now.has(page.id)),
316
+ ...retried.published
317
+ ],
318
+ drafts: [
319
+ ...previous.drafts.filter((page)=>!now.has(page.id)),
320
+ ...retried.drafts
321
+ ]
322
+ };
323
+ }
176
324
  /**
177
325
  * Publishes the pages a guided site start built. Never throws for a page; a
178
326
  * Firestore failure of the publish write itself reports every page as a
@@ -199,7 +347,9 @@ function navEntriesOf(outputs) {
199
347
  };
200
348
  if (!host.exists || !pages.length) return result;
201
349
  const before = _extends({}, (_host_get = host.get('screens')) != null ? _host_get : {});
202
- const placeholder = host.get('defaultHomeScreenId') || null;
350
+ const starter = host.get('defaultHomeScreenId') || null;
351
+ // A retry never takes `/` from the starter (AGL-3730): it holds it like any page.
352
+ const placeholder = input.retry ? null : starter;
203
353
  const routing = _extends({}, before);
204
354
  const entries = {};
205
355
  const accepted = [];
@@ -318,6 +468,14 @@ function navEntriesOf(outputs) {
318
468
  } else if (placeholder && entries[placeholder]) {
319
469
  hostUpdates['defaultHomeScreenId'] = FieldValue.delete();
320
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;
321
479
  batch.update(hostRef, hostUpdates);
322
480
  for (const page of accepted){
323
481
  batch.set(hostRef.collection('screens').doc(page.id), {
@@ -346,7 +504,7 @@ function navEntriesOf(outputs) {
346
504
  ],
347
505
  createdAt: FieldValue.serverTimestamp(),
348
506
  attempts: 0
349
- }, layoutWrites.length ? {
507
+ }, entireHost ? {
350
508
  entireHost: true
351
509
  } : {}));
352
510
  try {
@@ -376,14 +534,15 @@ function navEntriesOf(outputs) {
376
534
  hostId
377
535
  ],
378
536
  reason: 'guided AI site start published'
379
- }, layoutWrites.length ? {} : {
537
+ }, entireHost ? {} : {
380
538
  paths: {
381
539
  [hostId]: paths
382
540
  }
383
541
  }));
384
542
  if (dropped.complete) await outboxRef.delete().catch(()=>undefined);
385
- // A site that came alive with nobody's browser on the publish (AGL-1589).
386
- await sendSitePublished({
543
+ // A site that came alive with nobody's browser on the publish (AGL-1589),
544
+ // announced once: a retry's pages join a site already announced (AGL-3730).
545
+ if (!input.retry) await sendSitePublished({
387
546
  hostId,
388
547
  firstPublish: isFirstPublishedRoute(before, placeholder)
389
548
  });
@@ -396,9 +555,21 @@ function navEntriesOf(outputs) {
396
555
  */ async function aiNavigationLayoutWrite(firestore, input) {
397
556
  var _input_storeLinks;
398
557
  if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length && !input.storeLinks) return null;
399
- 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);
400
571
  const layoutIds = new Set();
401
- for (const id of input.pageIds){
572
+ for (const id of pageIds){
402
573
  var _ref;
403
574
  const screen = await hostRef.collection('screens').doc(id).get();
404
575
  const versionId = screen.get('versionId');
@@ -406,18 +577,9 @@ function navEntriesOf(outputs) {
406
577
  const layoutId = (_ref = version == null ? void 0 : version.get('layoutId')) != null ? _ref : screen.get('layoutId');
407
578
  if (typeof layoutId === 'string' && layoutId) layoutIds.add(layoutId);
408
579
  }
409
- if (layoutIds.size !== 1) return null;
410
- const [layoutId] = [
580
+ return layoutIds.size === 1 ? [
411
581
  ...layoutIds
412
- ];
413
- return aiLayoutVersionWrite(firestore, {
414
- hostId: input.hostId,
415
- layoutId,
416
- transform: (nodes)=>aiLayoutWithNavigation(nodes, input),
417
- storeLinks: (_input_storeLinks = input.storeLinks) != null ? _input_storeLinks : null,
418
- jobId: input.jobId,
419
- now: input.now
420
- });
582
+ ][0] : null;
421
583
  }
422
584
  /**
423
585
  * The new live version of one layout: `transform` applied to the version
@@ -426,6 +588,7 @@ function navEntriesOf(outputs) {
426
588
  * to the live version otherwise; a draft with nothing more to add becomes
427
589
  * the live version as it is. `null` when nothing changes.
428
590
  */ async function aiLayoutVersionWrite(firestore, input) {
591
+ var _aiLayoutWithoutFrameHollows, _input_displayName;
429
592
  var _input_storeLinks;
430
593
  const { layoutId } = input;
431
594
  const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(layoutId);
@@ -446,6 +609,15 @@ function navEntriesOf(outputs) {
446
609
  const added = aiLayoutWithStoreLinks(next != null ? next : nodes, links.links, links.screenPaths);
447
610
  next = (_added_nodes = added.nodes) != null ? _added_nodes : next;
448
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
+ }
449
621
  if (!next) return fromDraft && draftRef ? {
450
622
  layoutRef,
451
623
  versionRef: draftRef,
@@ -460,7 +632,7 @@ function navEntriesOf(outputs) {
460
632
  version: {
461
633
  layoutId,
462
634
  hostId: input.hostId,
463
- displayName: 'Navigation for the new pages',
635
+ displayName: (_input_displayName = input.displayName) != null ? _input_displayName : 'Navigation for the new pages',
464
636
  nodes: Buffer.from(packed),
465
637
  createdAt: input.now,
466
638
  updatedAt: input.now,