@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.273

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 (132) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +4 -3
  3. package/src/lib/activity/ai-activity-actions.js +3 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +18 -0
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
  8. package/src/lib/components/ai-site-import-job.component.js +707 -0
  9. package/src/lib/components/ai-site-import-job.component.js.map +1 -0
  10. package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
  11. package/src/lib/components/ai-site-import-start.component.js +335 -0
  12. package/src/lib/components/ai-site-import-start.component.js.map +1 -0
  13. package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
  14. package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
  15. package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
  16. package/src/lib/components/ai-site-start-card.component.js +329 -252
  17. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  18. package/src/lib/jobs/ai-empty-site-sweep.d.ts +12 -1
  19. package/src/lib/jobs/ai-empty-site-sweep.js +15 -3
  20. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -1
  21. package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
  22. package/src/lib/jobs/ai-job-drafts.js +24 -0
  23. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  24. package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
  25. package/src/lib/jobs/ai-job-import-step.js +809 -0
  26. package/src/lib/jobs/ai-job-import-step.js.map +1 -0
  27. package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
  28. package/src/lib/jobs/ai-job-page-language.js +62 -4
  29. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  30. package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
  31. package/src/lib/jobs/ai-job-page-step.js +26 -4
  32. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  33. package/src/lib/jobs/ai-job-seo-step.js +8 -4
  34. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-site-datasets.d.ts +39 -1
  36. package/src/lib/jobs/ai-job-site-datasets.js +111 -15
  37. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  38. package/src/lib/jobs/ai-job-site-step.d.ts +12 -3
  39. package/src/lib/jobs/ai-job-site-step.js +85 -11
  40. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  41. package/src/lib/jobs/ai-job-text-step.d.ts +15 -0
  42. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  43. package/src/lib/jobs/ai-jobs.d.ts +8 -0
  44. package/src/lib/jobs/ai-jobs.js +19 -3
  45. package/src/lib/jobs/ai-jobs.js.map +1 -1
  46. package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
  47. package/src/lib/jobs/ai-layout-owner-photos.js +49 -2
  48. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  49. package/src/lib/jobs/ai-layout-stock-photos.d.ts +35 -0
  50. package/src/lib/jobs/ai-layout-stock-photos.js +114 -43
  51. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  52. package/src/lib/jobs/ai-site-late-photos.d.ts +143 -0
  53. package/src/lib/jobs/ai-site-late-photos.js +486 -0
  54. package/src/lib/jobs/ai-site-late-photos.js.map +1 -0
  55. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  56. package/src/lib/jobs/ai-site-publish.js +123 -5
  57. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  58. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
  59. package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
  60. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  61. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.d.ts +83 -0
  62. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js +1553 -0
  63. package/src/lib/jobs/fixtures/ai-site-late-photos-laurel-fork.js.map +1 -0
  64. package/src/lib/layout-language/ai-layout-pictures.d.ts +50 -13
  65. package/src/lib/layout-language/ai-layout-pictures.js +81 -19
  66. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  67. package/src/lib/model/ai-credit-estimate.d.ts +20 -0
  68. package/src/lib/model/ai-credit-estimate.js +13 -0
  69. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  70. package/src/lib/model/ai-job-activity.d.ts +2 -2
  71. package/src/lib/model/ai-job-activity.js +6 -4
  72. package/src/lib/model/ai-job-activity.js.map +1 -1
  73. package/src/lib/model/ai-job-notice.js +18 -4
  74. package/src/lib/model/ai-job-notice.js.map +1 -1
  75. package/src/lib/model/ai-jobs.types.d.ts +39 -2
  76. package/src/lib/model/ai-jobs.types.js +2 -1
  77. package/src/lib/model/ai-jobs.types.js.map +1 -1
  78. package/src/lib/model/ai-site-import-view.d.ts +130 -0
  79. package/src/lib/model/ai-site-import-view.js +182 -0
  80. package/src/lib/model/ai-site-import-view.js.map +1 -0
  81. package/src/lib/model/ai-site-import.d.ts +302 -0
  82. package/src/lib/model/ai-site-import.js +526 -0
  83. package/src/lib/model/ai-site-import.js.map +1 -0
  84. package/src/lib/model/ai-site-photos.d.ts +119 -0
  85. package/src/lib/model/ai-site-photos.js +128 -0
  86. package/src/lib/model/ai-site-photos.js.map +1 -0
  87. package/src/lib/model/ai-site-start.d.ts +22 -9
  88. package/src/lib/model/ai-site-start.js +64 -23
  89. package/src/lib/model/ai-site-start.js.map +1 -1
  90. package/src/lib/runtime/ai-doctrine-validators.d.ts +3 -1
  91. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  92. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  93. package/src/lib/runtime/seo-fields.d.ts +29 -4
  94. package/src/lib/runtime/seo-fields.js +58 -11
  95. package/src/lib/runtime/seo-fields.js.map +1 -1
  96. package/src/lib/server/ai-jobs-route.js +11 -0
  97. package/src/lib/server/ai-jobs-route.js.map +1 -1
  98. package/src/lib/server/ai-site-import-route.d.ts +89 -0
  99. package/src/lib/server/ai-site-import-route.js +356 -0
  100. package/src/lib/server/ai-site-import-route.js.map +1 -0
  101. package/src/lib/server.js +14 -0
  102. package/src/lib/server.js.map +1 -1
  103. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
  104. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
  105. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
  106. package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
  107. package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
  108. package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
  109. package/src/lib/site-import/site-import-crawl.d.ts +163 -0
  110. package/src/lib/site-import/site-import-crawl.js +467 -0
  111. package/src/lib/site-import/site-import-crawl.js.map +1 -0
  112. package/src/lib/site-import/site-import-flag.d.ts +23 -0
  113. package/src/lib/site-import/site-import-flag.js +23 -0
  114. package/src/lib/site-import/site-import-flag.js.map +1 -0
  115. package/src/lib/site-import/site-import-html.d.ts +94 -0
  116. package/src/lib/site-import/site-import-html.js +462 -0
  117. package/src/lib/site-import/site-import-html.js.map +1 -0
  118. package/src/lib/site-import/site-import-images.d.ts +54 -0
  119. package/src/lib/site-import/site-import-images.js +204 -0
  120. package/src/lib/site-import/site-import-images.js.map +1 -0
  121. package/src/lib/site-import/site-import-page.d.ts +48 -0
  122. package/src/lib/site-import/site-import-page.js +150 -0
  123. package/src/lib/site-import/site-import-page.js.map +1 -0
  124. package/src/lib/site-import/site-import-review.d.ts +40 -0
  125. package/src/lib/site-import/site-import-review.js +149 -0
  126. package/src/lib/site-import/site-import-review.js.map +1 -0
  127. package/src/lib/site-import/site-import-robots.d.ts +52 -0
  128. package/src/lib/site-import/site-import-robots.js +126 -0
  129. package/src/lib/site-import/site-import-robots.js.map +1 -0
  130. package/src/lib/tools/ai-seo-tool.d.ts +25 -1
  131. package/src/lib/tools/ai-seo-tool.js +61 -0
  132. package/src/lib/tools/ai-seo-tool.js.map +1 -1
@@ -0,0 +1,809 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /*==========================================
18
+ * THE `import` JOB: BRING MY SITE, IN REVIEWED WAVES (AGL-3976).
19
+ *
20
+ * One step, many passes, each one unit of work (`aiSiteImportNextWork`):
21
+ *
22
+ * 1. READ the proved site (no model): the crawler reads it as AglynBot,
23
+ * main pages first, up to the plan's page count; the inventory is cut
24
+ * into waves (`model/ai-site-import.ts`), every page gets a ledger row,
25
+ * and Trust & Safety gets its row. The job then WAITS (`needs_review`,
26
+ * reason `wave`): nothing is brought in until the person starts wave 1.
27
+ * 2. START A WAVE (no model), on the person's say-so: the proof is still
28
+ * current, the day's limit has room, the plan still has room; the
29
+ * wave's pages are read again, fresh, and a page that became a sign-in
30
+ * page, went away or is now off limits to AglynBot is left out.
31
+ * 3. THE HEADER AND FOOTER, in the first wave: the layout step's own pass,
32
+ * linking the wave's pages.
33
+ * 4. EACH PAGE: its photos into the media library (no model, deduped by
34
+ * source key), then the page step's own passes in the layout language,
35
+ * told the page's text FENCED as untrusted data.
36
+ * 5. CLOSE THE WAVE: the job waits again, with the wave's pages as drafts
37
+ * to review, until the person starts the next one. The last wave ends
38
+ * the job.
39
+ *
40
+ * Every page is an item of the job's ledger, so a page that fails on our
41
+ * side gives back its own spend (the machine's `refundBuildCredits`) and the
42
+ * wave goes on. Nothing here publishes: every page is a draft, and the site
43
+ * stays unpublished until a member publishes it, which they can only reach
44
+ * once the import, behind its ownership proof, has written something.
45
+ *=========================================*/ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
46
+ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
47
+ import { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
48
+ import { listDomainOwnershipClaims, normalizeOwnershipDomain } from "@aglyn/tenant-data-admin/server/domain-ownership";
49
+ import { getOrgForUser } from "@aglyn/tenant-data-admin/server/organizations";
50
+ import { consumeRateLimit } from "@aglyn/tenant-data-admin/server/rate-limit-store";
51
+ import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
52
+ import { AI_SITE_IMPORT_DOMAIN_INPUT, AI_SITE_IMPORT_FRAME_SLOT, AI_SITE_IMPORT_LEFT_OUT_COPY, AI_SITE_IMPORT_MAX_PAGES, AI_SITE_IMPORT_MIN_WORDS, AI_SITE_IMPORT_WAVE_PAGES, aiSiteImportInventory, aiSiteImportItemOpen, aiSiteImportLedger, aiSiteImportNextWork, aiSiteImportPageSettled, aiSiteImportPagesRead, aiSiteImportPhotosIn, aiSiteImportWaveClosed, aiSiteImportWaveMessage, aiSiteImportWaveStarted } from "../model/ai-site-import.js";
53
+ import { aiModelForStep } from "../providers/routing.js";
54
+ import { SITE_IMPORT_RELEASE_FLAG } from "../site-import/site-import-flag.js";
55
+ import { crawlSiteForImport } from "../site-import/site-import-crawl.js";
56
+ import { bringInSiteImportPhotos } from "../site-import/site-import-images.js";
57
+ import { siteImportLeftOutReason, siteImportListedPage, siteImportPageRead, siteImportPageWords } from "../site-import/site-import-page.js";
58
+ import { fileSiteImportReview } from "../site-import/site-import-review.js";
59
+ import { aiUnitErrorRetryable, aiUnitFailure, aiUnitSpend } from "./ai-build-unit-outcome.js";
60
+ import { registerAiJobAdmission } from "./ai-job-admission.js";
61
+ import { AI_JOB_STEP_OVERHEAD_MS } from "./ai-job-budget.js";
62
+ import { readAiScreenRoom } from "./ai-job-drafts.js";
63
+ import { aiUnspentOutcome } from "./ai-job-generation.js";
64
+ import { AI_LAYOUT_PAGE_PHOTOS_INPUT } from "./ai-layout-owner-photos.js";
65
+ import { AI_LAYOUT_SITE_PAGES_INPUT, AI_LAYOUT_SITE_PAGES_MAX } from "./ai-job-layout-site-pages.js";
66
+ import { AI_JOB_PAGE_STEP_MINIMUM_MS } from "./ai-job-page-budget.js";
67
+ import { AI_LAYOUT_IMPORTED_PAGE_INPUT, AI_LAYOUT_LANGUAGE_INPUT } from "./ai-job-page-language.js";
68
+ import { aiSitePageWritten } from "./ai-job-site-step.js";
69
+ import { AI_STOCK_PHOTO_PAGES_INPUT } from "./ai-layout-stock-photos.js";
70
+ import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
71
+ import { aiApplyJobItemRecord, aiJobStepRunMinimumMs, aiJobStepRunnerFor, registerAiJobStep, registerAiJobStepPasses } from "./ai-jobs.js";
72
+ /** Waves (the read included) one workspace may start in a day. */ export const SITE_IMPORT_WAVES_PER_ORG_PER_DAY = 12;
73
+ const DAY_MS = 24 * 60 * 60 * 1000;
74
+ /** The rate-limit key a workspace's imports share. */ export function siteImportRateKey(orgId) {
75
+ return `ai-import:${orgId}`;
76
+ }
77
+ /** The crawl's clock when the import reads the whole site. */ export const AI_SITE_IMPORT_READ_MS = 150000;
78
+ /** The crawl's clock when a wave reads its own pages again. */ export const AI_SITE_IMPORT_WAVE_READ_MS = 90000;
79
+ /** The clock for one page's photos: each is fetched and stored through the library's door. */ export const AI_SITE_IMPORT_PHOTOS_MS = 90000;
80
+ /** What the step's own reads and writes around a pass take: the claims, the room, the row. */ const OWN_READS_MS = 10000;
81
+ /** The least time each pass that asks no model needs. */ export const AI_SITE_IMPORT_PASS_MINIMUM_MS = {
82
+ read: AI_SITE_IMPORT_READ_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS,
83
+ wave: AI_SITE_IMPORT_WAVE_READ_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS,
84
+ photos: AI_SITE_IMPORT_PHOTOS_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS
85
+ };
86
+ /**
87
+ * The passes one import may take: a read, then for every page its photos,
88
+ * its answer and its listing, every wave's start and the header and footer,
89
+ * with room for re-asks.
90
+ */ export const AI_SITE_IMPORT_MAX_PASSES = AI_SITE_IMPORT_MAX_PAGES * 4 + 60;
91
+ /** Customer-safe sentences. */ export const AI_SITE_IMPORT_COPY = {
92
+ noSite: 'Open the site to bring your website into.',
93
+ badDomain: 'This website’s address cannot be brought in.',
94
+ ownership: (domain)=>`Verify that you own ${domain} before bringing it in. A proof is good for 30 days; verify again, then start this wave again.`,
95
+ limit: `This workspace has started as many site waves as it can today (${SITE_IMPORT_WAVES_PER_ORG_PER_DAY}). Start this wave again tomorrow.`,
96
+ robots: (domain)=>`The robots.txt file of ${domain} could not be read just now, so nothing was read. Try again in a few minutes.`,
97
+ nothing: (domain)=>`No page of ${domain} could be read. Check that the site is up, then try again.`,
98
+ unavailable: 'Bringing a site in is not available on this workspace right now.',
99
+ frameUnavailable: 'The header and footer could not be built here; your pages use the site’s own layout.',
100
+ pageUnavailable: 'Pages cannot be built here right now.',
101
+ pageNotWritten: 'This page could not be written from its old page. Its spend is given back.',
102
+ plan: 'Bringing an existing site in is included on every paid plan.',
103
+ role: 'Only the workspace owner or an admin can bring a site in.'
104
+ };
105
+ async function readOwnership(orgId, domain) {
106
+ const claims = await listDomainOwnershipClaims(orgId);
107
+ const claim = claims.find((entry)=>entry.domain === domain);
108
+ if (!claim) return null;
109
+ return {
110
+ current: claim.current,
111
+ method: claim.provenMethod,
112
+ provenAt: claim.provenAt,
113
+ detail: claim.provenDetail
114
+ };
115
+ }
116
+ async function readSiteName(firestore, hostId) {
117
+ const host = await firestore.collection('hosts').doc(hostId).get();
118
+ const name = host.get('displayName');
119
+ return typeof name === 'string' ? name.trim() : '';
120
+ }
121
+ /** The proved domain an import job is for, or `null`. */ export function aiSiteImportDomainOf(job) {
122
+ var _ref;
123
+ var _job_inputs;
124
+ return normalizeOwnershipDomain(String((_ref = (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_SITE_IMPORT_DOMAIN_INPUT]) != null ? _ref : '')).domain;
125
+ }
126
+ /*------------------------------------------
127
+ * The jobs a unit is handed to
128
+ *-----------------------------------------*/ const pathOf = (slug)=>slug === '/' ? '/' : `/${slug}`;
129
+ /** A plan as a delegated unit reads it: confirmed, with what it builds and nothing else. */ function unitPlan(job, input) {
130
+ var _input_create, _input_screens;
131
+ return {
132
+ reuse: [],
133
+ create: (_input_create = input.create) != null ? _input_create : [],
134
+ screens: (_input_screens = input.screens) != null ? _input_screens : [],
135
+ labels: {},
136
+ status: 'confirmed',
137
+ proposedAt: job.createdAt,
138
+ confirmedAt: job.createdAt,
139
+ confirmedBy: job.createdBy
140
+ };
141
+ }
142
+ const delivered = (items, slot)=>items.some((row)=>row.slot === slot && (row.status === 'succeeded' || row.status === 'degraded'));
143
+ /**
144
+ * The pages the header and a page may link: the ones of this wave and those
145
+ * before it that are in or still coming, home first, at most the header's
146
+ * seven. A page of a later wave is not linked: it may never be brought in.
147
+ */ export function aiSiteImportLinkedPages(state, items, wave) {
148
+ const rows = new Map(items.map((row)=>[
149
+ row.slot,
150
+ row
151
+ ]));
152
+ const pages = state.pages.filter((page)=>page.wave <= wave && !page.refused).filter((page)=>{
153
+ const row = rows.get(page.slot);
154
+ return !row || row.status === 'pending' || row.status === 'running' || row.status === 'succeeded' || row.status === 'degraded';
155
+ }).map((page)=>({
156
+ id: page.draftId,
157
+ label: page.title,
158
+ slug: pathOf(page.slug)
159
+ }));
160
+ return [
161
+ ...pages.filter((page)=>page.slug === '/'),
162
+ ...pages.filter((page)=>page.slug !== '/')
163
+ ].slice(0, AI_LAYOUT_SITE_PAGES_MAX);
164
+ }
165
+ /** The header and footer's job: a layout of the site's name, linking the wave's pages, in the layout language. */ export function aiSiteImportFrameJob(job, state, wave, items) {
166
+ var _ref;
167
+ var _state_frame;
168
+ const draftId = (_ref = (_state_frame = state.frame) == null ? void 0 : _state_frame.draftId) != null ? _ref : `${job.$id}-${AI_SITE_IMPORT_FRAME_SLOT}`;
169
+ const creation = {
170
+ kind: 'layout',
171
+ name: 'Header and footer',
172
+ why: `The header and footer every page brought in from ${state.domain} renders inside.`,
173
+ duplicateOf: null,
174
+ fields: [
175
+ 'header',
176
+ 'nav',
177
+ 'main',
178
+ 'footer'
179
+ ],
180
+ id: draftId
181
+ };
182
+ return _extends({}, job, {
183
+ $id: draftId,
184
+ kind: 'layout',
185
+ steps: [],
186
+ outputs: [],
187
+ items: null,
188
+ siteImport: null,
189
+ plan: unitPlan(job, {
190
+ create: [
191
+ creation
192
+ ]
193
+ }),
194
+ inputs: {
195
+ originJobId: job.$id,
196
+ businessName: state.siteName,
197
+ [AI_LAYOUT_LANGUAGE_INPUT]: true,
198
+ [AI_LAYOUT_SITE_PAGES_INPUT]: aiSiteImportLinkedPages(state, items, wave.index)
199
+ },
200
+ brief: `Build the header and footer of “${state.siteName}”, a website brought in from ${state.domain}.`.slice(0, AI_JOB_BRIEF_MAX_CHARS)
201
+ });
202
+ }
203
+ /** One page's job: a page job's plan of one screen, its sections from the old page's headings, its text fenced. */ export function aiSiteImportPageJob(job, state, page, items) {
204
+ var _ref, _ref1;
205
+ var _page_read, _page_read1, _page_photos;
206
+ const sections = (((_page_read = page.read) == null ? void 0 : _page_read.sections.length) ? page.read.sections : [
207
+ 'Introduction'
208
+ ]).map((name)=>({
209
+ name,
210
+ uses: [],
211
+ items: 0
212
+ }));
213
+ const screen = {
214
+ title: page.title,
215
+ slug: pathOf(page.slug),
216
+ // Inside the header and footer this import built, once it is in.
217
+ layout: state.frame && delivered(items, AI_SITE_IMPORT_FRAME_SLOT) ? state.frame.draftId : null,
218
+ template: null,
219
+ duplicateOf: null,
220
+ nav: false,
221
+ seoTitle: page.title,
222
+ seoDescription: '',
223
+ sections,
224
+ record: null,
225
+ id: page.draftId
226
+ };
227
+ // The other pages already in, whose stock photos this page does not repeat.
228
+ const others = state.pages.filter((entry)=>entry.slot !== page.slot && delivered(items, entry.slot)).map((entry)=>entry.draftId);
229
+ return _extends({}, job, {
230
+ $id: page.draftId,
231
+ kind: 'page',
232
+ steps: [],
233
+ outputs: [],
234
+ items: null,
235
+ siteImport: null,
236
+ plan: unitPlan(job, {
237
+ screens: [
238
+ screen
239
+ ]
240
+ }),
241
+ inputs: _extends({
242
+ originJobId: job.$id,
243
+ businessName: state.siteName,
244
+ [AI_LAYOUT_LANGUAGE_INPUT]: true,
245
+ [AI_LAYOUT_SITE_PAGES_INPUT]: aiSiteImportLinkedPages(state, items, page.wave),
246
+ [AI_LAYOUT_IMPORTED_PAGE_INPUT]: {
247
+ url: page.url,
248
+ text: (_ref = (_page_read1 = page.read) == null ? void 0 : _page_read1.text) != null ? _ref : ''
249
+ },
250
+ [AI_LAYOUT_PAGE_PHOTOS_INPUT]: (_ref1 = (_page_photos = page.photos) == null ? void 0 : _page_photos.assets) != null ? _ref1 : []
251
+ }, others.length ? {
252
+ [AI_STOCK_PHOTO_PAGES_INPUT]: others
253
+ } : {}),
254
+ brief: `Bring in the page “${page.title}” of ${state.domain} as a page of “${state.siteName}”.`.slice(0, AI_JOB_BRIEF_MAX_CHARS)
255
+ });
256
+ }
257
+ /** The least time the import's next pass needs: a read's, a photo pass's, or the time of the step its unit is handed to. */ export function aiSiteImportRunMinimumMs(job, runnerFor = aiJobStepRunnerFor) {
258
+ var _job_siteImport, _job_items;
259
+ const state = (_job_siteImport = job.siteImport) != null ? _job_siteImport : null;
260
+ const items = (_job_items = job.items) != null ? _job_items : [];
261
+ const work = aiSiteImportNextWork(state, items);
262
+ switch(work.kind){
263
+ case 'inventory':
264
+ return AI_SITE_IMPORT_PASS_MINIMUM_MS.read;
265
+ case 'start-wave':
266
+ case 'read':
267
+ return AI_SITE_IMPORT_PASS_MINIMUM_MS.wave;
268
+ case 'images':
269
+ return AI_SITE_IMPORT_PASS_MINIMUM_MS.photos;
270
+ case 'frame':
271
+ return runnerFor('layout') && state ? aiJobStepRunMinimumMs(aiSiteImportFrameJob(job, state, work.wave, items)) : 0;
272
+ case 'page':
273
+ return runnerFor('page') && state ? aiJobStepRunMinimumMs(aiSiteImportPageJob(job, state, work.page, items)) : 0;
274
+ default:
275
+ return 0;
276
+ }
277
+ }
278
+ /*------------------------------------------
279
+ * The step
280
+ *-----------------------------------------*/ const waveReview = (message)=>({
281
+ reason: 'wave',
282
+ message,
283
+ findings: []
284
+ });
285
+ /** The rows a wave's read left out, settled as not brought in, with the reason. */ function withLeftOutRows(items, reads) {
286
+ return items.map((row)=>{
287
+ const read = reads.get(row.slot);
288
+ return typeof read === 'string' && (row.status === 'pending' || row.status === 'running') ? _extends({}, row, {
289
+ status: 'skipped',
290
+ note: AI_SITE_IMPORT_LEFT_OUT_COPY[read]
291
+ }) : row;
292
+ });
293
+ }
294
+ /** Whether every row of the wave is settled, its header and footer included. */ function waveSettled(state, wave, items) {
295
+ if (wave.frame && state.frame && aiSiteImportItemOpen(items, AI_SITE_IMPORT_FRAME_SLOT)) return false;
296
+ return wave.slots.every((slot)=>!aiSiteImportItemOpen(items, slot));
297
+ }
298
+ export function createAiJobImportStep(deps = {}) {
299
+ var _deps_crawl, _deps_runnerFor, _deps_ownership, _deps_rateLimit, _deps_releaseFlagOn, _deps_ingest, _deps_readRoom, _deps_readSiteName, _deps_fileReview, _deps_pageWritten, _deps_mintId, _deps_clock;
300
+ const crawl = (_deps_crawl = deps.crawl) != null ? _deps_crawl : crawlSiteForImport;
301
+ const runnerFor = (_deps_runnerFor = deps.runnerFor) != null ? _deps_runnerFor : aiJobStepRunnerFor;
302
+ const ownershipOf = (_deps_ownership = deps.ownership) != null ? _deps_ownership : readOwnership;
303
+ const rateLimit = (_deps_rateLimit = deps.rateLimit) != null ? _deps_rateLimit : consumeRateLimit;
304
+ const flagOn = (_deps_releaseFlagOn = deps.releaseFlagOn) != null ? _deps_releaseFlagOn : (orgId)=>isServerReleaseFlagOnForOrg(SITE_IMPORT_RELEASE_FLAG, orgId);
305
+ const ingestOf = (_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest;
306
+ const readRoom = (_deps_readRoom = deps.readRoom) != null ? _deps_readRoom : readAiScreenRoom;
307
+ const siteNameOf = (_deps_readSiteName = deps.readSiteName) != null ? _deps_readSiteName : readSiteName;
308
+ const fileReview = (_deps_fileReview = deps.fileReview) != null ? _deps_fileReview : fileSiteImportReview;
309
+ const pageWritten = (_deps_pageWritten = deps.pageWritten) != null ? _deps_pageWritten : aiSitePageWritten;
310
+ const mintId = (_deps_mintId = deps.mintId) != null ? _deps_mintId : createResourceUid;
311
+ const clock = (_deps_clock = deps.clock) != null ? _deps_clock : Date.now;
312
+ return async (context)=>{
313
+ var _job_siteImport, _job_items;
314
+ const { job, firestore, now, signal } = context;
315
+ const model = aiModelForStep('job.page');
316
+ const unspent = (rest = {})=>_extends({}, aiUnspentOutcome(model), rest);
317
+ const hostId = job.hostId;
318
+ if (!hostId) return unspent({
319
+ failure: AI_SITE_IMPORT_COPY.noSite
320
+ });
321
+ const domain = aiSiteImportDomainOf(job);
322
+ if (!domain) return unspent({
323
+ failure: AI_SITE_IMPORT_COPY.badDomain
324
+ });
325
+ const state = (_job_siteImport = job.siteImport) != null ? _job_siteImport : null;
326
+ const items = (_job_items = job.items) != null ? _job_items : [];
327
+ const work = aiSiteImportNextWork(state, items);
328
+ const nowIso = now.toISOString();
329
+ const review = (event, next, ledger, wave)=>fileReview(firestore, _extends({
330
+ orgId: job.orgId,
331
+ hostId,
332
+ jobId: job.$id,
333
+ state: next,
334
+ items: ledger,
335
+ event,
336
+ nowMs: now.getTime()
337
+ }, wave !== undefined ? {
338
+ wave
339
+ } : {}));
340
+ /** Closes the wave when this pass settled its last row: the review, or the job's end after the last wave. */ const closing = async (next, ledger, wave, outcome)=>{
341
+ var _closed_waves_find;
342
+ if (!waveSettled(next, wave, ledger)) return _extends({}, outcome, {
343
+ siteImport: next,
344
+ continue: true
345
+ });
346
+ const closed = aiSiteImportWaveClosed(next, wave.index, nowIso);
347
+ await review('wave-finished', closed, ledger, wave.index);
348
+ const waiting = ((_closed_waves_find = closed.waves.find((entry)=>entry.index === wave.index)) == null ? void 0 : _closed_waves_find.status) === 'review';
349
+ return waiting ? _extends({}, outcome, {
350
+ siteImport: closed,
351
+ review: waveReview(aiSiteImportWaveMessage(closed, ledger))
352
+ }) : _extends({}, outcome, {
353
+ siteImport: closed
354
+ });
355
+ };
356
+ /** The proof, the flag and the day's limit, asked before anything is read. */ const gate = async ()=>{
357
+ if (!await flagOn(job.orgId)) return unspent({
358
+ review: waveReview(AI_SITE_IMPORT_COPY.unavailable)
359
+ });
360
+ const ownership = await ownershipOf(job.orgId, domain);
361
+ if (!(ownership == null ? void 0 : ownership.current)) {
362
+ return unspent({
363
+ review: {
364
+ reason: 'ownership',
365
+ message: AI_SITE_IMPORT_COPY.ownership(domain),
366
+ findings: []
367
+ }
368
+ });
369
+ }
370
+ const window = await rateLimit(siteImportRateKey(job.orgId), {
371
+ limit: SITE_IMPORT_WAVES_PER_ORG_PER_DAY,
372
+ windowMs: DAY_MS
373
+ });
374
+ if (!window.allowed) return unspent({
375
+ review: {
376
+ reason: 'limit',
377
+ message: AI_SITE_IMPORT_COPY.limit,
378
+ findings: []
379
+ }
380
+ });
381
+ return {
382
+ ownership
383
+ };
384
+ };
385
+ // ── 1. Read the site ──
386
+ if (work.kind === 'inventory') {
387
+ var _context_org;
388
+ const passed = await gate();
389
+ if (!('ownership' in passed)) return passed;
390
+ const room = await readRoom(firestore, {
391
+ hostId,
392
+ org: (_context_org = context.org) != null ? _context_org : null
393
+ });
394
+ const left = room.limit === null ? AI_SITE_IMPORT_MAX_PAGES : Math.max(0, room.limit - room.used);
395
+ const crawled = await crawl(_extends({
396
+ domain,
397
+ // A wave's worth past the room, so what the plan leaves out is said.
398
+ maxPages: Math.min(AI_SITE_IMPORT_MAX_PAGES, left + AI_SITE_IMPORT_WAVE_PAGES),
399
+ timeBudgetMs: AI_SITE_IMPORT_READ_MS,
400
+ shallowFirst: true
401
+ }, signal ? {
402
+ signal
403
+ } : {}), deps.crawlSeams);
404
+ if (crawled.refused) return unspent({
405
+ failure: AI_SITE_IMPORT_COPY.badDomain
406
+ });
407
+ if (crawled.stoppedBy === 'robots-unreachable') return unspent({
408
+ review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
409
+ });
410
+ if (!crawled.pages.length) return unspent({
411
+ review: waveReview(AI_SITE_IMPORT_COPY.nothing(domain))
412
+ });
413
+ const inventory = aiSiteImportInventory({
414
+ domain,
415
+ siteName: await siteNameOf(firestore, hostId),
416
+ ownership: {
417
+ method: passed.ownership.method,
418
+ provenAt: passed.ownership.provenAt,
419
+ detail: passed.ownership.detail
420
+ },
421
+ readAt: nowIso,
422
+ pages: crawled.pages.map(siteImportListedPage),
423
+ menu: siteImportMenu(crawled),
424
+ skipped: crawled.skipped.map((entry)=>({
425
+ url: entry.url,
426
+ reason: siteImportLeftOutReason(entry.reason)
427
+ })),
428
+ skippedOverflow: crawled.skippedOverflow,
429
+ stoppedBy: crawled.stoppedBy,
430
+ pageLimit: room.limit,
431
+ existingPages: room.used,
432
+ mintId
433
+ });
434
+ const ledger = aiSiteImportLedger(inventory);
435
+ const reviewId = await review('read', inventory, ledger);
436
+ const next = _extends({}, inventory, {
437
+ reviewId
438
+ });
439
+ return unspent({
440
+ items: ledger,
441
+ siteImport: next,
442
+ review: waveReview(aiSiteImportWaveMessage(next, ledger))
443
+ });
444
+ }
445
+ if (!state) return unspent({
446
+ failure: AI_SITE_IMPORT_COPY.badDomain
447
+ });
448
+ if (work.kind === 'done') return unspent();
449
+ // ── 2. Start a wave ──
450
+ if (work.kind === 'start-wave') {
451
+ var _context_org1;
452
+ const passed = await gate();
453
+ if (!('ownership' in passed)) return passed;
454
+ const wave = work.wave;
455
+ const pages = wave.slots.map((slot)=>state.pages.find((page)=>page.slot === slot)).filter((page)=>!!page);
456
+ const reads = new Map();
457
+ // Never past the plan: the room is read again, since pages may have been added since the site was read.
458
+ const room = await readRoom(firestore, {
459
+ hostId,
460
+ org: (_context_org1 = context.org) != null ? _context_org1 : null
461
+ });
462
+ let left = room.limit === null ? Number.POSITIVE_INFINITY : Math.max(0, room.limit - room.used);
463
+ const toRead = [];
464
+ for (const page of pages){
465
+ if (left > 0) {
466
+ toRead.push(page);
467
+ left -= 1;
468
+ } else {
469
+ reads.set(page.slot, 'plan-limit');
470
+ }
471
+ }
472
+ if (toRead.length) {
473
+ const crawled = await crawl(_extends({
474
+ domain,
475
+ maxPages: toRead.length,
476
+ only: toRead.map((page)=>page.url),
477
+ timeBudgetMs: AI_SITE_IMPORT_WAVE_READ_MS
478
+ }, signal ? {
479
+ signal
480
+ } : {}), deps.crawlSeams);
481
+ if (crawled.stoppedBy === 'robots-unreachable') return unspent({
482
+ review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
483
+ });
484
+ // A read the clock cut short leaves its other pages for the next pass, never "unreadable".
485
+ for (const [slot, read] of siteImportWaveReads(toRead, crawled, nowIso, {
486
+ cutShort: crawled.stoppedBy === 'time'
487
+ })){
488
+ reads.set(slot, read);
489
+ }
490
+ }
491
+ let ledger = withLeftOutRows(items, reads);
492
+ // A wave none of whose pages could be read builds no header for them.
493
+ const anyRead = [
494
+ ...reads.values()
495
+ ].some((read)=>typeof read !== 'string');
496
+ if (wave.frame && !anyRead && reads.size === pages.length) {
497
+ ledger = ledger.map((row)=>row.slot === AI_SITE_IMPORT_FRAME_SLOT && (row.status === 'pending' || row.status === 'running') ? _extends({}, row, {
498
+ status: 'skipped',
499
+ note: 'Not built: no page of this wave could be read.'
500
+ }) : row);
501
+ }
502
+ const started = aiSiteImportWaveStarted(state, wave.index, nowIso, reads);
503
+ await review('wave-started', started, ledger, wave.index);
504
+ const running = started.waves.find((entry)=>entry.index === wave.index);
505
+ return closing(started, ledger, running, unspent({
506
+ items: ledger
507
+ }));
508
+ }
509
+ const wave = work.wave;
510
+ // ── 2b. The rest of a wave's read, where the clock cut it short ──
511
+ if (work.kind === 'read') {
512
+ const crawled = await crawl(_extends({
513
+ domain,
514
+ maxPages: work.pages.length,
515
+ only: work.pages.map((page)=>page.url),
516
+ timeBudgetMs: AI_SITE_IMPORT_WAVE_READ_MS
517
+ }, signal ? {
518
+ signal
519
+ } : {}), deps.crawlSeams);
520
+ if (crawled.stoppedBy === 'robots-unreachable') return unspent({
521
+ review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
522
+ });
523
+ const reads = siteImportWaveReads(work.pages, crawled, nowIso, {
524
+ cutShort: crawled.stoppedBy === 'time'
525
+ });
526
+ const ledger = withLeftOutRows(items, reads);
527
+ return closing(aiSiteImportPagesRead(state, reads), ledger, wave, unspent({
528
+ items: ledger
529
+ }));
530
+ }
531
+ // ── 3. The header and footer ──
532
+ if (work.kind === 'frame') {
533
+ const runner = runnerFor('layout');
534
+ if (!runner) {
535
+ const item = {
536
+ slot: AI_SITE_IMPORT_FRAME_SLOT,
537
+ status: 'skipped',
538
+ note: AI_SITE_IMPORT_COPY.frameUnavailable
539
+ };
540
+ return closing(state, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
541
+ item
542
+ }));
543
+ }
544
+ const derived = aiSiteImportFrameJob(job, state, wave, items);
545
+ return runUnit(AI_SITE_IMPORT_FRAME_SLOT, runner, derived, state, null);
546
+ }
547
+ // ── 4a. A page's photos ──
548
+ if (work.kind === 'images') {
549
+ var _ref, _ref1;
550
+ var _work_page_read, _work_page_read1;
551
+ const photos = (signal == null ? void 0 : signal.aborted) ? {
552
+ brought: 0,
553
+ reused: 0,
554
+ skipped: (_ref = (_work_page_read = work.page.read) == null ? void 0 : _work_page_read.images.length) != null ? _ref : 0,
555
+ assets: []
556
+ } : await bringInSiteImportPhotos(_extends({
557
+ hostId,
558
+ uid: job.createdBy,
559
+ domain,
560
+ images: (_ref1 = (_work_page_read1 = work.page.read) == null ? void 0 : _work_page_read1.images) != null ? _ref1 : [],
561
+ deadlineAt: clock() + AI_SITE_IMPORT_PHOTOS_MS
562
+ }, signal ? {
563
+ signal
564
+ } : {}), _extends({
565
+ ingest: ingestOf()
566
+ }, deps.fetchImage ? {
567
+ fetch: deps.fetchImage
568
+ } : {}));
569
+ return unspent({
570
+ item: {
571
+ slot: work.page.slot,
572
+ status: 'running'
573
+ },
574
+ siteImport: aiSiteImportPhotosIn(state, work.page.slot, photos),
575
+ continue: true
576
+ });
577
+ }
578
+ // ── 4b. The page ──
579
+ if (work.kind === 'page') {
580
+ const runner = runnerFor('page');
581
+ if (!runner) {
582
+ const item = {
583
+ slot: work.page.slot,
584
+ status: 'failed',
585
+ failure: {
586
+ ours: true,
587
+ reason: 'unavailable',
588
+ message: AI_SITE_IMPORT_COPY.pageUnavailable
589
+ }
590
+ };
591
+ return closing(state, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
592
+ item
593
+ }));
594
+ }
595
+ return runUnit(work.page.slot, runner, aiSiteImportPageJob(job, state, work.page, items), state, work.page);
596
+ }
597
+ // ── 5. Close the wave ──
598
+ return closing(state, items, wave, unspent());
599
+ /** One delegated pass: the unit's spend is the pass's, what it came to is its row. */ async function runUnit(slot, runner, derived, current, page) {
600
+ let outcome;
601
+ try {
602
+ outcome = await runner(_extends({}, context, {
603
+ job: derived
604
+ }));
605
+ } catch (error) {
606
+ var _ref;
607
+ // A provider that will answer later, a budget that ended, a cancel: the machine's.
608
+ if (aiUnitErrorRetryable(error) || ((_ref = signal == null ? void 0 : signal.aborted) != null ? _ref : false)) throw error;
609
+ console.error('ai site import unit threw', {
610
+ orgId: job.orgId,
611
+ jobId: job.$id,
612
+ slot,
613
+ error
614
+ });
615
+ const item = {
616
+ slot,
617
+ status: 'failed',
618
+ failure: {
619
+ ours: true,
620
+ reason: 'provider',
621
+ message: AI_SITE_IMPORT_COPY.pageNotWritten
622
+ }
623
+ };
624
+ return closing(current, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
625
+ item
626
+ }));
627
+ }
628
+ const spent = aiUnitSpend(outcome);
629
+ const stopped = aiUnitFailure(slot, outcome);
630
+ if ((stopped == null ? void 0 : stopped.status) === 'running') return _extends({}, spent, {
631
+ outputs: [],
632
+ item: stopped,
633
+ siteImport: current,
634
+ continue: true
635
+ });
636
+ let item;
637
+ if (stopped) {
638
+ item = stopped;
639
+ } else if (page && derived.plan) {
640
+ // A page counts as in only when its sections are in it (AGL-3596).
641
+ const screen = derived.plan.screens[0];
642
+ const written = await pageWritten(firestore, {
643
+ hostId: hostId,
644
+ unitJobId: derived.$id,
645
+ screen,
646
+ draftId: page.draftId,
647
+ layoutLanguage: true
648
+ });
649
+ item = written ? {
650
+ slot,
651
+ status: 'succeeded',
652
+ outputs: spent.outputs.map((output)=>output.id)
653
+ } : {
654
+ slot,
655
+ status: 'failed',
656
+ failure: {
657
+ ours: true,
658
+ reason: 'step-failure',
659
+ message: AI_SITE_IMPORT_COPY.pageNotWritten
660
+ }
661
+ };
662
+ } else {
663
+ item = {
664
+ slot,
665
+ status: 'succeeded',
666
+ outputs: spent.outputs.map((output)=>output.id)
667
+ };
668
+ }
669
+ const settledState = page ? aiSiteImportPageSettled(current, slot) : current;
670
+ return closing(settledState, aiApplyJobItemRecord(items, item, 0, now), wave, _extends({}, spent, {
671
+ outputs: item.status === 'succeeded' ? spent.outputs : [],
672
+ item
673
+ }));
674
+ }
675
+ };
676
+ }
677
+ /** The home page's menu, as same-site paths in its order. */ export function siteImportMenu(crawled) {
678
+ var _crawled_pages_find, _ref;
679
+ const home = (_crawled_pages_find = crawled.pages.find((page)=>page.foundBy === 'home')) != null ? _crawled_pages_find : crawled.pages[0];
680
+ const paths = [];
681
+ for (const link of (_ref = home == null ? void 0 : home.navLinks) != null ? _ref : []){
682
+ try {
683
+ const url = new URL(link.url);
684
+ if (!crawled.origins.includes(url.origin)) continue;
685
+ const path = url.pathname.replace(/\/+$/, '') || '/';
686
+ if (!paths.includes(path)) paths.push(path);
687
+ } catch (unused) {
688
+ // A menu entry that is not an address names no page.
689
+ }
690
+ }
691
+ return paths;
692
+ }
693
+ /**
694
+ * What a wave's read came to for each page: the page read again, or why it
695
+ * is left out now. Matched by address; a page the site sent to another of its
696
+ * own addresses is matched in the order the wave asked for it.
697
+ */ export function siteImportWaveReads(asked, crawled, at, options = {}) {
698
+ const reads = new Map();
699
+ const pathKey = (url)=>{
700
+ try {
701
+ const parsed = new URL(url);
702
+ return `${parsed.pathname.replace(/\/+$/, '') || '/'}${parsed.search}`;
703
+ } catch (unused) {
704
+ return url;
705
+ }
706
+ };
707
+ const pages = new Map(crawled.pages.map((page)=>[
708
+ pathKey(page.url),
709
+ page
710
+ ]));
711
+ const skipped = new Map(crawled.skipped.map((entry)=>[
712
+ pathKey(entry.url),
713
+ entry.reason
714
+ ]));
715
+ const unmatched = [
716
+ ...crawled.pages
717
+ ];
718
+ const take = (page)=>{
719
+ const index = unmatched.indexOf(page);
720
+ if (index !== -1) unmatched.splice(index, 1);
721
+ };
722
+ const pending = [];
723
+ for (const page of asked){
724
+ const found = pages.get(pathKey(page.url));
725
+ if (found) {
726
+ take(found);
727
+ reads.set(page.slot, siteImportReadOrEmpty(found, at));
728
+ continue;
729
+ }
730
+ const reason = skipped.get(pathKey(page.url));
731
+ if (reason) {
732
+ reads.set(page.slot, siteImportLeftOutReason(reason));
733
+ continue;
734
+ }
735
+ pending.push(page);
736
+ }
737
+ for (const page of pending){
738
+ const found = unmatched.shift();
739
+ if (found) reads.set(page.slot, siteImportReadOrEmpty(found, at));
740
+ else if (!options.cutShort) reads.set(page.slot, 'unreadable');
741
+ }
742
+ return reads;
743
+ }
744
+ function siteImportReadOrEmpty(page, at) {
745
+ if (siteImportPageWords(page) < AI_SITE_IMPORT_MIN_WORDS && !page.images.length) return 'empty';
746
+ return siteImportPageRead(page, at);
747
+ }
748
+ /**
749
+ * What an import asks before it exists and before each wave (AGL-3976): the
750
+ * release flag, a site to bring it into, a paid plan, the owner or an admin,
751
+ * and a CURRENT proof that the workspace owns the domain. Nothing is spent on
752
+ * a refusal, and the day's limit is the step's to count, once a wave runs.
753
+ */ export function createAiSiteImportAdmission(deps = {}) {
754
+ var _deps_releaseFlagOn, _deps_ownership, _deps_memberRole;
755
+ const flagOn = (_deps_releaseFlagOn = deps.releaseFlagOn) != null ? _deps_releaseFlagOn : (orgId)=>isServerReleaseFlagOnForOrg(SITE_IMPORT_RELEASE_FLAG, orgId);
756
+ const ownershipOf = (_deps_ownership = deps.ownership) != null ? _deps_ownership : readOwnership;
757
+ const memberRole = (_deps_memberRole = deps.memberRole) != null ? _deps_memberRole : async (uid, orgId)=>{
758
+ var _ref;
759
+ var _member, _this;
760
+ return (_ref = (_this = await getOrgForUser(uid, orgId)) == null ? void 0 : (_member = _this.member) == null ? void 0 : _member.role) != null ? _ref : null;
761
+ };
762
+ return async (context)=>{
763
+ var _context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT;
764
+ if (!await flagOn(context.orgId)) return {
765
+ status: 404,
766
+ error: 'Not found'
767
+ };
768
+ if (!context.hostId && !context.siteToCome) return {
769
+ status: 400,
770
+ error: AI_SITE_IMPORT_COPY.noSite
771
+ };
772
+ if (resolveEffectivePlan(context.org) === 'free') return {
773
+ status: 403,
774
+ error: AI_SITE_IMPORT_COPY.plan
775
+ };
776
+ const role = context.uid ? await memberRole(context.uid, context.orgId) : null;
777
+ if (role !== 'owner' && role !== 'admin') return {
778
+ status: 403,
779
+ error: AI_SITE_IMPORT_COPY.role
780
+ };
781
+ const { domain, error } = normalizeOwnershipDomain(String((_context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT = context.inputs[AI_SITE_IMPORT_DOMAIN_INPUT]) != null ? _context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT : ''));
782
+ if (!domain) return {
783
+ status: 400,
784
+ error: error != null ? error : AI_SITE_IMPORT_COPY.badDomain
785
+ };
786
+ const ownership = await ownershipOf(context.orgId, domain);
787
+ if (!(ownership == null ? void 0 : ownership.current)) return {
788
+ status: 403,
789
+ error: AI_SITE_IMPORT_COPY.ownership(domain)
790
+ };
791
+ return null;
792
+ };
793
+ }
794
+ /**
795
+ * The import kind's registrations (AGL-3976), by a call from `server.ts`,
796
+ * never by an import: its runner with each pass's least time, its pass bound
797
+ * and its admission.
798
+ */ export function registerAiSiteImportJob(deps = {}) {
799
+ var _deps_runnerFor;
800
+ const runnerFor = (_deps_runnerFor = deps.runnerFor) != null ? _deps_runnerFor : aiJobStepRunnerFor;
801
+ registerAiJobStep('import', createAiJobImportStep(deps), {
802
+ minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,
803
+ minimumMsFor: (job)=>aiSiteImportRunMinimumMs(job, runnerFor)
804
+ });
805
+ registerAiJobStepPasses('import', AI_SITE_IMPORT_MAX_PASSES);
806
+ registerAiJobAdmission('import', createAiSiteImportAdmission());
807
+ }
808
+
809
+ //# sourceMappingURL=ai-job-import-step.js.map