@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,204 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /*==========================================
17
+ * AN IMPORTED PAGE'S PHOTOS, INTO THE SITE'S OWN MEDIA LIBRARY (AGL-3976).
18
+ *
19
+ * Every picture goes through the media library's server door
20
+ * (`pluginMediaIngest().ingest`), which checks the member, the site's
21
+ * lockdown, the bytes' structure, the takedown list and the storage band
22
+ * exactly as an upload does. This module only fetches and names:
23
+ *
24
+ * - **Never a second copy.** A picture's SOURCE KEY is `import:` and a hash
25
+ * of its address with any fragment and tracking parameter removed. The
26
+ * library is asked for that key first (`findImportedPhoto`); a logo every
27
+ * page shows is stored once and placed everywhere. An ingest that cannot
28
+ * answer the question stores nothing, rather than risk the second copy.
29
+ * - **The safe fetch.** Bytes are read through `fetchPublicTextDetailed`,
30
+ * the SSRF-guarded fetch the crawler uses (public https only, the socket
31
+ * pinned, every redirect checked again), as AglynBot, capped in size and
32
+ * time, and only as JPEG, PNG, WebP or GIF: the raster types the library
33
+ * takes from a server.
34
+ *=========================================*/ import { _ as _extends } from "@swc/helpers/_/_extends";
35
+ import { createHash } from "node:crypto";
36
+ import { fetchPublicTextDetailed } from "../jobs/ai-theme-brand-inputs.js";
37
+ import { AGLYN_BOT_USER_AGENT } from "./site-import-crawl.js";
38
+ /** The largest picture brought in. */ export const SITE_IMPORT_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
39
+ /** A picture smaller than this is a spacer or a tracking pixel, not a photo. */ export const SITE_IMPORT_IMAGE_MIN_BYTES = 1024;
40
+ /** One picture's own clock. */ export const SITE_IMPORT_IMAGE_TIMEOUT_MS = 8000;
41
+ const RASTER = /^image\/(?:jpeg|jpg|pjpeg|png|webp|gif)\b/i;
42
+ const NOT_A_PHOTO = /\.(?:svg|ico|bmp|tiff?|avif|heic)$/i;
43
+ const TRACKING_PARAM = /^(?:utm_[a-z]+|fbclid|gclid|msclkid|_ga|ref)$/i;
44
+ const EXTENSION = {
45
+ 'image/jpeg': 'jpg',
46
+ 'image/jpg': 'jpg',
47
+ 'image/pjpeg': 'jpg',
48
+ 'image/png': 'png',
49
+ 'image/webp': 'webp',
50
+ 'image/gif': 'gif'
51
+ };
52
+ /** A picture's address as it is fetched and keyed: https, no fragment or tracking; `null` for one that is not a photo. */ export function siteImportImageUrl(raw) {
53
+ let url;
54
+ try {
55
+ url = new URL(String(raw != null ? raw : '').trim());
56
+ } catch (unused) {
57
+ return null;
58
+ }
59
+ if (url.protocol === 'http:') url.protocol = 'https:';
60
+ if (url.protocol !== 'https:' || url.username || url.password) return null;
61
+ if (url.port && url.port !== '443') return null;
62
+ if (NOT_A_PHOTO.test(url.pathname)) return null;
63
+ url.hash = '';
64
+ for (const key of [
65
+ ...url.searchParams.keys()
66
+ ]){
67
+ if (TRACKING_PARAM.test(key)) url.searchParams.delete(key);
68
+ }
69
+ return url.toString();
70
+ }
71
+ /** The key a site's library holds an imported picture by. */ export function siteImportImageKey(url) {
72
+ return `import:${createHash('sha256').update(url).digest('hex').slice(0, 32)}`;
73
+ }
74
+ /** A file name for the library, from the picture's address and type. */ export function siteImportImageFileName(url, contentType) {
75
+ var _EXTENSION_type;
76
+ let base = 'photo';
77
+ try {
78
+ var _pathname_split_filter_pop;
79
+ const last = decodeURIComponent((_pathname_split_filter_pop = new URL(url).pathname.split('/').filter(Boolean).pop()) != null ? _pathname_split_filter_pop : '');
80
+ base = last.replace(/\.[a-z0-9]+$/i, '').replace(/[^a-zA-Z0-9 _-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'photo';
81
+ } catch (unused) {
82
+ // An address that does not decode keeps the plain name.
83
+ }
84
+ const type = contentType.split(';')[0].trim().toLowerCase();
85
+ return `${base.slice(0, 80)}.${(_EXTENSION_type = EXTENSION[type]) != null ? _EXTENSION_type : 'jpg'}`;
86
+ }
87
+ /**
88
+ * Brings one page's pictures into the site's library, reusing any the
89
+ * library already holds by key. Never throws for anything a picture does.
90
+ */ export async function bringInSiteImportPhotos(input, deps) {
91
+ var _deps_now, _deps_fetch;
92
+ const now = (_deps_now = deps.now) != null ? _deps_now : Date.now;
93
+ const fetchBytes = (_deps_fetch = deps.fetch) != null ? _deps_fetch : fetchPublicTextDetailed;
94
+ const assets = [];
95
+ let brought = 0;
96
+ let reused = 0;
97
+ let skipped = 0;
98
+ const ingest = deps.ingest;
99
+ // No library door, or one that cannot say what it already holds: nothing
100
+ // is stored, because a picture stored without that check could be a copy.
101
+ if (!(ingest == null ? void 0 : ingest.findImportedPhoto)) {
102
+ return {
103
+ brought,
104
+ reused,
105
+ skipped: input.images.length,
106
+ assets
107
+ };
108
+ }
109
+ const seen = new Set();
110
+ for (const image of input.images){
111
+ var _input_signal;
112
+ const url = siteImportImageUrl(image.url);
113
+ if (!url || seen.has(url)) {
114
+ if (!url) skipped += 1;
115
+ continue;
116
+ }
117
+ seen.add(url);
118
+ if (((_input_signal = input.signal) == null ? void 0 : _input_signal.aborted) || now() >= input.deadlineAt) {
119
+ skipped += 1;
120
+ continue;
121
+ }
122
+ const sourceKey = siteImportImageKey(url);
123
+ try {
124
+ const held = await ingest.findImportedPhoto({
125
+ hostId: input.hostId,
126
+ sourceKey
127
+ });
128
+ if (held) {
129
+ reused += 1;
130
+ assets.push(_extends({
131
+ mediaId: held.mediaId,
132
+ src: held.src
133
+ }, held.width ? {
134
+ width: held.width
135
+ } : {}, held.height ? {
136
+ height: held.height
137
+ } : {}, held.alt || image.alt ? {
138
+ alt: held.alt || image.alt
139
+ } : {}));
140
+ continue;
141
+ }
142
+ const fetched = await fetchBytes(url, _extends({
143
+ maxBytes: SITE_IMPORT_IMAGE_MAX_BYTES,
144
+ accept: RASTER,
145
+ acceptHeader: 'image/webp,image/png,image/jpeg,image/gif;q=0.9',
146
+ userAgent: AGLYN_BOT_USER_AGENT,
147
+ timeoutMs: Math.max(1, Math.min(SITE_IMPORT_IMAGE_TIMEOUT_MS, input.deadlineAt - now())),
148
+ refuseOver: true,
149
+ binary: true
150
+ }, input.signal ? {
151
+ signal: input.signal
152
+ } : {}));
153
+ if (!fetched.ok || !fetched.data || fetched.data.byteLength < SITE_IMPORT_IMAGE_MIN_BYTES) {
154
+ skipped += 1;
155
+ continue;
156
+ }
157
+ const contentType = fetched.contentType.split(';')[0].trim().toLowerCase().replace('image/jpg', 'image/jpeg').replace('image/pjpeg', 'image/jpeg');
158
+ const stored = await ingest.ingest(_extends({
159
+ hostId: input.hostId,
160
+ uid: input.uid,
161
+ fileName: siteImportImageFileName(url, contentType),
162
+ contentType,
163
+ bytes: fetched.data
164
+ }, image.alt ? {
165
+ alt: image.alt.slice(0, 250)
166
+ } : {}, {
167
+ importedFrom: {
168
+ key: sourceKey,
169
+ url,
170
+ domain: input.domain
171
+ }
172
+ }));
173
+ if (!stored.ok) {
174
+ skipped += 1;
175
+ continue;
176
+ }
177
+ brought += 1;
178
+ assets.push(_extends({
179
+ mediaId: stored.mediaId,
180
+ src: stored.src
181
+ }, stored.width ? {
182
+ width: stored.width
183
+ } : {}, stored.height ? {
184
+ height: stored.height
185
+ } : {}, image.alt ? {
186
+ alt: image.alt.slice(0, 250)
187
+ } : {}));
188
+ } catch (error) {
189
+ console.warn('[site-import] a photo could not be brought in', {
190
+ hostId: input.hostId,
191
+ error
192
+ });
193
+ skipped += 1;
194
+ }
195
+ }
196
+ return {
197
+ brought,
198
+ reused,
199
+ skipped,
200
+ assets
201
+ };
202
+ }
203
+
204
+ //# sourceMappingURL=site-import-images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-images.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * AN IMPORTED PAGE'S PHOTOS, INTO THE SITE'S OWN MEDIA LIBRARY (AGL-3976).\n *\n * Every picture goes through the media library's server door\n * (`pluginMediaIngest().ingest`), which checks the member, the site's\n * lockdown, the bytes' structure, the takedown list and the storage band\n * exactly as an upload does. This module only fetches and names:\n *\n * - **Never a second copy.** A picture's SOURCE KEY is `import:` and a hash\n * of its address with any fragment and tracking parameter removed. The\n * library is asked for that key first (`findImportedPhoto`); a logo every\n * page shows is stored once and placed everywhere. An ingest that cannot\n * answer the question stores nothing, rather than risk the second copy.\n * - **The safe fetch.** Bytes are read through `fetchPublicTextDetailed`,\n * the SSRF-guarded fetch the crawler uses (public https only, the socket\n * pinned, every redirect checked again), as AglynBot, capped in size and\n * time, and only as JPEG, PNG, WebP or GIF: the raster types the library\n * takes from a server.\n *=========================================*/\n\nimport type { PluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { createHash } from 'node:crypto'\nimport {\n fetchPublicTextDetailed,\n type PublicFetchOptions,\n type PublicFetchResult,\n} from '../jobs/ai-theme-brand-inputs'\nimport type { AiSiteImportPage, AiSiteImportPhoto } from '../model/ai-site-import'\nimport { AGLYN_BOT_USER_AGENT } from './site-import-crawl'\n\n/** The largest picture brought in. */\nexport const SITE_IMPORT_IMAGE_MAX_BYTES = 8 * 1024 * 1024\n\n/** A picture smaller than this is a spacer or a tracking pixel, not a photo. */\nexport const SITE_IMPORT_IMAGE_MIN_BYTES = 1_024\n\n/** One picture's own clock. */\nexport const SITE_IMPORT_IMAGE_TIMEOUT_MS = 8_000\n\nconst RASTER = /^image\\/(?:jpeg|jpg|pjpeg|png|webp|gif)\\b/i\n\nconst NOT_A_PHOTO = /\\.(?:svg|ico|bmp|tiff?|avif|heic)$/i\n\nconst TRACKING_PARAM = /^(?:utm_[a-z]+|fbclid|gclid|msclkid|_ga|ref)$/i\n\nconst EXTENSION: Record<string, string> = {\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/pjpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\n/** A picture's address as it is fetched and keyed: https, no fragment or tracking; `null` for one that is not a photo. */\nexport function siteImportImageUrl(raw: string): string | null {\n let url: URL\n try {\n url = new URL(String(raw ?? '').trim())\n } catch {\n return null\n }\n if (url.protocol === 'http:') url.protocol = 'https:'\n if (url.protocol !== 'https:' || url.username || url.password) return null\n if (url.port && url.port !== '443') return null\n if (NOT_A_PHOTO.test(url.pathname)) return null\n url.hash = ''\n for (const key of [...url.searchParams.keys()]) {\n if (TRACKING_PARAM.test(key)) url.searchParams.delete(key)\n }\n return url.toString()\n}\n\n/** The key a site's library holds an imported picture by. */\nexport function siteImportImageKey(url: string): string {\n return `import:${createHash('sha256').update(url).digest('hex').slice(0, 32)}`\n}\n\n/** A file name for the library, from the picture's address and type. */\nexport function siteImportImageFileName(url: string, contentType: string): string {\n let base = 'photo'\n try {\n const last = decodeURIComponent(new URL(url).pathname.split('/').filter(Boolean).pop() ?? '')\n base = last.replace(/\\.[a-z0-9]+$/i, '').replace(/[^a-zA-Z0-9 _-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'photo'\n } catch {\n // An address that does not decode keeps the plain name.\n }\n const type = contentType.split(';')[0].trim().toLowerCase()\n return `${base.slice(0, 80)}.${EXTENSION[type] ?? 'jpg'}`\n}\n\nexport interface SiteImportPhotosInput {\n hostId: string\n /** The member the photos are stored as: the job's creator. */\n uid: string\n domain: string\n images: ReadonlyArray<{ url: string; alt: string }>\n signal?: AbortSignal\n /** When the pass must stop asking for more. */\n deadlineAt: number\n}\n\nexport interface SiteImportPhotosDeps {\n ingest: PluginMediaIngest | null\n fetch?: (url: string, options: PublicFetchOptions) => Promise<PublicFetchResult>\n now?: () => number\n}\n\n/**\n * Brings one page's pictures into the site's library, reusing any the\n * library already holds by key. Never throws for anything a picture does.\n */\nexport async function bringInSiteImportPhotos(\n input: SiteImportPhotosInput,\n deps: SiteImportPhotosDeps,\n): Promise<NonNullable<AiSiteImportPage['photos']>> {\n const now = deps.now ?? Date.now\n const fetchBytes = deps.fetch ?? fetchPublicTextDetailed\n const assets: AiSiteImportPhoto[] = []\n let brought = 0\n let reused = 0\n let skipped = 0\n const ingest = deps.ingest\n // No library door, or one that cannot say what it already holds: nothing\n // is stored, because a picture stored without that check could be a copy.\n if (!ingest?.findImportedPhoto) {\n return { brought, reused, skipped: input.images.length, assets }\n }\n const seen = new Set<string>()\n for (const image of input.images) {\n const url = siteImportImageUrl(image.url)\n if (!url || seen.has(url)) {\n if (!url) skipped += 1\n continue\n }\n seen.add(url)\n if (input.signal?.aborted || now() >= input.deadlineAt) {\n skipped += 1\n continue\n }\n const sourceKey = siteImportImageKey(url)\n try {\n const held = await ingest.findImportedPhoto({ hostId: input.hostId, sourceKey })\n if (held) {\n reused += 1\n assets.push({\n mediaId: held.mediaId,\n src: held.src,\n ...(held.width ? { width: held.width } : {}),\n ...(held.height ? { height: held.height } : {}),\n ...(held.alt || image.alt ? { alt: held.alt || image.alt } : {}),\n })\n continue\n }\n const fetched = await fetchBytes(url, {\n maxBytes: SITE_IMPORT_IMAGE_MAX_BYTES,\n accept: RASTER,\n acceptHeader: 'image/webp,image/png,image/jpeg,image/gif;q=0.9',\n userAgent: AGLYN_BOT_USER_AGENT,\n timeoutMs: Math.max(1, Math.min(SITE_IMPORT_IMAGE_TIMEOUT_MS, input.deadlineAt - now())),\n refuseOver: true,\n binary: true,\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (!fetched.ok || !fetched.data || fetched.data.byteLength < SITE_IMPORT_IMAGE_MIN_BYTES) {\n skipped += 1\n continue\n }\n const contentType = fetched.contentType.split(';')[0].trim().toLowerCase().replace('image/jpg', 'image/jpeg').replace('image/pjpeg', 'image/jpeg')\n const stored = await ingest.ingest({\n hostId: input.hostId,\n uid: input.uid,\n fileName: siteImportImageFileName(url, contentType),\n contentType,\n bytes: fetched.data,\n ...(image.alt ? { alt: image.alt.slice(0, 250) } : {}),\n importedFrom: { key: sourceKey, url, domain: input.domain },\n })\n if (!stored.ok) {\n skipped += 1\n continue\n }\n brought += 1\n assets.push({\n mediaId: stored.mediaId,\n src: stored.src,\n ...(stored.width ? { width: stored.width } : {}),\n ...(stored.height ? { height: stored.height } : {}),\n ...(image.alt ? { alt: image.alt.slice(0, 250) } : {}),\n })\n } catch (error) {\n console.warn('[site-import] a photo could not be brought in', { hostId: input.hostId, error })\n skipped += 1\n }\n }\n return { brought, reused, skipped, assets }\n}\n"],"names":["createHash","fetchPublicTextDetailed","AGLYN_BOT_USER_AGENT","SITE_IMPORT_IMAGE_MAX_BYTES","SITE_IMPORT_IMAGE_MIN_BYTES","SITE_IMPORT_IMAGE_TIMEOUT_MS","RASTER","NOT_A_PHOTO","TRACKING_PARAM","EXTENSION","siteImportImageUrl","raw","url","URL","String","trim","protocol","username","password","port","test","pathname","hash","key","searchParams","keys","delete","toString","siteImportImageKey","update","digest","slice","siteImportImageFileName","contentType","base","last","decodeURIComponent","split","filter","Boolean","pop","replace","type","toLowerCase","bringInSiteImportPhotos","input","deps","now","Date","fetchBytes","fetch","assets","brought","reused","skipped","ingest","findImportedPhoto","images","length","seen","Set","image","has","add","signal","aborted","deadlineAt","sourceKey","held","hostId","push","mediaId","src","width","height","alt","fetched","maxBytes","accept","acceptHeader","userAgent","timeoutMs","Math","max","min","refuseOver","binary","ok","data","byteLength","stored","uid","fileName","bytes","importedFrom","domain","error","console","warn"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type AiSiteImportLeftOutReason, type AiSiteImportListedPage, type AiSiteImportPageRead } from '../model/ai-site-import';
18
+ import type { SiteImportPage, SiteImportSkipReason } from './site-import-crawl';
19
+ /** A line of untrusted text safe to show outside a fence: neutralized, one line, cut short. */
20
+ export declare function siteImportLabel(text: string, max?: number): string;
21
+ /** How many words a page says, its title and headings included. */
22
+ export declare function siteImportPageWords(page: Pick<SiteImportPage, 'title' | 'blocks'>): number;
23
+ /**
24
+ * A page's name on the new site: "Home" for the home page, else its browser
25
+ * title without the old site's name after a separator ("About | Rosie's
26
+ * Bakery" is "About"), else its first heading.
27
+ */
28
+ export declare function siteImportPageTitle(page: Pick<SiteImportPage, 'path' | 'title' | 'blocks'>): string;
29
+ /** The page as the inventory lists it. */
30
+ export declare function siteImportListedPage(page: SiteImportPage): AiSiteImportListedPage;
31
+ /** The page's photos, the ones in its body first, each once. */
32
+ export declare function siteImportPageImages(page: Pick<SiteImportPage, 'blocks' | 'images'>): Array<{
33
+ url: string;
34
+ alt: string;
35
+ }>;
36
+ /**
37
+ * The page's sections, from its headings: the first is its opening (its
38
+ * title or first heading), then one for each second-level heading of its
39
+ * body, at most `AI_SITE_IMPORT_MAX_SECTIONS`. A page with no headings below
40
+ * its title but more to say than an opening gets a second section for it.
41
+ */
42
+ export declare function siteImportPageSections(page: Pick<SiteImportPage, 'title' | 'blocks'>): string[];
43
+ /** The page's text in order, as a prompt shows it inside its fence. */
44
+ export declare function siteImportPageText(page: Pick<SiteImportPage, 'title' | 'description' | 'blocks'>): string;
45
+ /** The page as a wave reads it. */
46
+ export declare function siteImportPageRead(page: SiteImportPage, at: string): AiSiteImportPageRead;
47
+ /** Why the crawler could not read a page, as an import leaves it out. */
48
+ export declare function siteImportLeftOutReason(reason: SiteImportSkipReason): AiSiteImportLeftOutReason;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /*==========================================
17
+ * ONE CRAWLED PAGE, AS AN IMPORT KEEPS IT (AGL-3976).
18
+ *
19
+ * The crawler's page (`SiteImportPage`) holds everything it extracted; an
20
+ * import keeps much less, and keeps it in two shapes:
21
+ *
22
+ * - **Listed**, for the inventory: its address, title, how it was found and
23
+ * how many words and photos it has. No text: an inventory of a whole site
24
+ * rides the job document.
25
+ * - **Read**, for a wave: its text in order, its sections from its headings,
26
+ * and its photos' addresses. The text is kept as the page says it and is
27
+ * fenced as untrusted data only where a prompt shows it; the section names
28
+ * and the title travel outside the fence, so they are neutralized (no
29
+ * fence marker, no hidden character) and cut short.
30
+ *=========================================*/ import { neutralizeUntrustedText } from "@aglyn/shared-util-tools/untrusted-text";
31
+ import { AI_SITE_IMPORT_IMAGES_PER_PAGE, AI_SITE_IMPORT_LABEL_MAX_CHARS, AI_SITE_IMPORT_MAX_SECTIONS, AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS } from "../model/ai-site-import.js";
32
+ /** A line of untrusted text safe to show outside a fence: neutralized, one line, cut short. */ export function siteImportLabel(text, max = AI_SITE_IMPORT_LABEL_MAX_CHARS) {
33
+ const line = neutralizeUntrustedText(text).replace(/\s+/g, ' ').trim();
34
+ return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
35
+ }
36
+ const wordsIn = (text)=>{
37
+ var _text_match;
38
+ return ((_text_match = text.match(RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+", "gu"))) != null ? _text_match : []).length;
39
+ };
40
+ /** How many words a page says, its title and headings included. */ export function siteImportPageWords(page) {
41
+ return page.blocks.reduce((total, block)=>total + (block.kind === 'image' ? 0 : wordsIn(block.text)), 0);
42
+ }
43
+ /** Between a page's own name and its site's in a browser title: "About | Rosie's Bakery". */ const TITLE_SEPARATOR = /\s+[|\u2013\u2014\u00b7:-]\s+/;
44
+ /**
45
+ * A page's name on the new site: "Home" for the home page, else its browser
46
+ * title without the old site's name after a separator ("About | Rosie's
47
+ * Bakery" is "About"), else its first heading.
48
+ */ export function siteImportPageTitle(page) {
49
+ var _split_, _page_title, _ref;
50
+ var _page_blocks_find;
51
+ if ((page.path.replace(/\/+$/, '') || '/') === '/') return 'Home';
52
+ const own = siteImportLabel((_split_ = ((_page_title = page.title) != null ? _page_title : '').split(TITLE_SEPARATOR)[0]) != null ? _split_ : '');
53
+ if (own.length >= 2) return own;
54
+ return siteImportLabel((_ref = (_page_blocks_find = page.blocks.find((block)=>block.kind === 'heading')) == null ? void 0 : _page_blocks_find.text) != null ? _ref : '');
55
+ }
56
+ /** The page as the inventory lists it. */ export function siteImportListedPage(page) {
57
+ return {
58
+ url: page.url,
59
+ path: page.path,
60
+ title: siteImportPageTitle(page),
61
+ foundBy: page.foundBy,
62
+ words: siteImportPageWords(page),
63
+ images: page.images.length
64
+ };
65
+ }
66
+ /** The page's photos, the ones in its body first, each once. */ export function siteImportPageImages(page) {
67
+ const seen = new Set();
68
+ const images = [];
69
+ const add = (url, alt)=>{
70
+ if (!url || seen.has(url) || images.length >= AI_SITE_IMPORT_IMAGES_PER_PAGE) return;
71
+ seen.add(url);
72
+ images.push({
73
+ url,
74
+ alt: siteImportLabel(alt, 250)
75
+ });
76
+ };
77
+ for (const block of page.blocks)if (block.kind === 'image' && block.region === 'main') add(block.url, block.alt);
78
+ for (const image of page.images)add(image.url, image.alt);
79
+ return images;
80
+ }
81
+ /**
82
+ * The page's sections, from its headings: the first is its opening (its
83
+ * title or first heading), then one for each second-level heading of its
84
+ * body, at most `AI_SITE_IMPORT_MAX_SECTIONS`. A page with no headings below
85
+ * its title but more to say than an opening gets a second section for it.
86
+ */ export function siteImportPageSections(page) {
87
+ var _headings_find;
88
+ const main = page.blocks.filter((block)=>block.region === 'main');
89
+ const headings = main.filter((block)=>block.kind === 'heading');
90
+ const opening = siteImportLabel(((_headings_find = headings.find((block)=>block.level === 1)) == null ? void 0 : _headings_find.text) || page.title || '') || 'Introduction';
91
+ const sections = [
92
+ opening
93
+ ];
94
+ const seen = new Set([
95
+ opening.toLowerCase()
96
+ ]);
97
+ for (const heading of headings){
98
+ if (heading.level !== 2) continue;
99
+ const name = siteImportLabel(heading.text);
100
+ if (!name || seen.has(name.toLowerCase())) continue;
101
+ seen.add(name.toLowerCase());
102
+ sections.push(name);
103
+ if (sections.length >= AI_SITE_IMPORT_MAX_SECTIONS) break;
104
+ }
105
+ if (sections.length === 1 && siteImportPageWords({
106
+ title: '',
107
+ blocks: main
108
+ }) > 80) sections.push('Details');
109
+ return sections;
110
+ }
111
+ /** The page's text in order, as a prompt shows it inside its fence. */ export function siteImportPageText(page) {
112
+ const lines = [];
113
+ if (page.title) lines.push(`Title: ${page.title}`);
114
+ if (page.description) lines.push(`Description: ${page.description}`);
115
+ for (const block of page.blocks){
116
+ const where = block.region === 'main' ? '' : `(${block.region}) `;
117
+ if (block.kind === 'heading') lines.push(`${'#'.repeat(block.level)} ${block.text}`);
118
+ else if (block.kind === 'image') lines.push(`${where}[photo: ${block.alt || 'no description'}]`);
119
+ else if (block.kind === 'list-item') lines.push(`${where}- ${block.text}`);
120
+ else if (block.kind === 'quote') lines.push(`${where}> ${block.text}`);
121
+ else lines.push(`${where}${block.text}`);
122
+ }
123
+ const text = lines.join('\n');
124
+ return text.length > AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS ? text.slice(0, AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS) : text;
125
+ }
126
+ /** The page as a wave reads it. */ export function siteImportPageRead(page, at) {
127
+ return {
128
+ at,
129
+ title: siteImportPageTitle(page),
130
+ text: siteImportPageText(page),
131
+ sections: siteImportPageSections(page),
132
+ images: siteImportPageImages(page)
133
+ };
134
+ }
135
+ /** Why the crawler could not read a page, as an import leaves it out. */ export function siteImportLeftOutReason(reason) {
136
+ switch(reason){
137
+ case 'login-form':
138
+ return 'login-form';
139
+ case 'robots':
140
+ return 'robots';
141
+ case 'too-large':
142
+ return 'too-large';
143
+ case 'off-origin-redirect':
144
+ return 'off-site';
145
+ default:
146
+ return 'unreadable';
147
+ }
148
+ }
149
+
150
+ //# sourceMappingURL=site-import-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-page.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * ONE CRAWLED PAGE, AS AN IMPORT KEEPS IT (AGL-3976).\n *\n * The crawler's page (`SiteImportPage`) holds everything it extracted; an\n * import keeps much less, and keeps it in two shapes:\n *\n * - **Listed**, for the inventory: its address, title, how it was found and\n * how many words and photos it has. No text: an inventory of a whole site\n * rides the job document.\n * - **Read**, for a wave: its text in order, its sections from its headings,\n * and its photos' addresses. The text is kept as the page says it and is\n * fenced as untrusted data only where a prompt shows it; the section names\n * and the title travel outside the fence, so they are neutralized (no\n * fence marker, no hidden character) and cut short.\n *=========================================*/\n\nimport { neutralizeUntrustedText } from '@aglyn/shared-util-tools/untrusted-text'\nimport {\n AI_SITE_IMPORT_IMAGES_PER_PAGE,\n AI_SITE_IMPORT_LABEL_MAX_CHARS,\n AI_SITE_IMPORT_MAX_SECTIONS,\n AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS,\n type AiSiteImportLeftOutReason,\n type AiSiteImportListedPage,\n type AiSiteImportPageRead,\n} from '../model/ai-site-import'\nimport type { SiteImportPage, SiteImportSkipReason } from './site-import-crawl'\n\n/** A line of untrusted text safe to show outside a fence: neutralized, one line, cut short. */\nexport function siteImportLabel(text: string, max = AI_SITE_IMPORT_LABEL_MAX_CHARS): string {\n const line = neutralizeUntrustedText(text).replace(/\\s+/g, ' ').trim()\n return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line\n}\n\nconst wordsIn = (text: string) => (text.match(/[\\p{L}\\p{N}]+/gu) ?? []).length\n\n/** How many words a page says, its title and headings included. */\nexport function siteImportPageWords(page: Pick<SiteImportPage, 'title' | 'blocks'>): number {\n return page.blocks.reduce((total, block) => total + (block.kind === 'image' ? 0 : wordsIn(block.text)), 0)\n}\n\n/** Between a page's own name and its site's in a browser title: \"About | Rosie's Bakery\". */\nconst TITLE_SEPARATOR = /\\s+[|\\u2013\\u2014\\u00b7:-]\\s+/\n\n/**\n * A page's name on the new site: \"Home\" for the home page, else its browser\n * title without the old site's name after a separator (\"About | Rosie's\n * Bakery\" is \"About\"), else its first heading.\n */\nexport function siteImportPageTitle(page: Pick<SiteImportPage, 'path' | 'title' | 'blocks'>): string {\n if ((page.path.replace(/\\/+$/, '') || '/') === '/') return 'Home'\n const own = siteImportLabel((page.title ?? '').split(TITLE_SEPARATOR)[0] ?? '')\n if (own.length >= 2) return own\n return siteImportLabel(page.blocks.find((block) => block.kind === 'heading')?.text ?? '')\n}\n\n/** The page as the inventory lists it. */\nexport function siteImportListedPage(page: SiteImportPage): AiSiteImportListedPage {\n return {\n url: page.url,\n path: page.path,\n title: siteImportPageTitle(page),\n foundBy: page.foundBy,\n words: siteImportPageWords(page),\n images: page.images.length,\n }\n}\n\n/** The page's photos, the ones in its body first, each once. */\nexport function siteImportPageImages(page: Pick<SiteImportPage, 'blocks' | 'images'>): Array<{ url: string; alt: string }> {\n const seen = new Set<string>()\n const images: Array<{ url: string; alt: string }> = []\n const add = (url: string, alt: string) => {\n if (!url || seen.has(url) || images.length >= AI_SITE_IMPORT_IMAGES_PER_PAGE) return\n seen.add(url)\n images.push({ url, alt: siteImportLabel(alt, 250) })\n }\n for (const block of page.blocks) if (block.kind === 'image' && block.region === 'main') add(block.url, block.alt)\n for (const image of page.images) add(image.url, image.alt)\n return images\n}\n\n/**\n * The page's sections, from its headings: the first is its opening (its\n * title or first heading), then one for each second-level heading of its\n * body, at most `AI_SITE_IMPORT_MAX_SECTIONS`. A page with no headings below\n * its title but more to say than an opening gets a second section for it.\n */\nexport function siteImportPageSections(page: Pick<SiteImportPage, 'title' | 'blocks'>): string[] {\n const main = page.blocks.filter((block) => block.region === 'main')\n const headings = main.filter((block): block is Extract<typeof block, { kind: 'heading' }> => block.kind === 'heading')\n const opening = siteImportLabel(headings.find((block) => block.level === 1)?.text || page.title || '') || 'Introduction'\n const sections = [opening]\n const seen = new Set([opening.toLowerCase()])\n for (const heading of headings) {\n if (heading.level !== 2) continue\n const name = siteImportLabel(heading.text)\n if (!name || seen.has(name.toLowerCase())) continue\n seen.add(name.toLowerCase())\n sections.push(name)\n if (sections.length >= AI_SITE_IMPORT_MAX_SECTIONS) break\n }\n if (sections.length === 1 && siteImportPageWords({ title: '', blocks: main }) > 80) sections.push('Details')\n return sections\n}\n\n/** The page's text in order, as a prompt shows it inside its fence. */\nexport function siteImportPageText(page: Pick<SiteImportPage, 'title' | 'description' | 'blocks'>): string {\n const lines: string[] = []\n if (page.title) lines.push(`Title: ${page.title}`)\n if (page.description) lines.push(`Description: ${page.description}`)\n for (const block of page.blocks) {\n const where = block.region === 'main' ? '' : `(${block.region}) `\n if (block.kind === 'heading') lines.push(`${'#'.repeat(block.level)} ${block.text}`)\n else if (block.kind === 'image') lines.push(`${where}[photo: ${block.alt || 'no description'}]`)\n else if (block.kind === 'list-item') lines.push(`${where}- ${block.text}`)\n else if (block.kind === 'quote') lines.push(`${where}> ${block.text}`)\n else lines.push(`${where}${block.text}`)\n }\n const text = lines.join('\\n')\n return text.length > AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS ? text.slice(0, AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS) : text\n}\n\n/** The page as a wave reads it. */\nexport function siteImportPageRead(page: SiteImportPage, at: string): AiSiteImportPageRead {\n return {\n at,\n title: siteImportPageTitle(page),\n text: siteImportPageText(page),\n sections: siteImportPageSections(page),\n images: siteImportPageImages(page),\n }\n}\n\n/** Why the crawler could not read a page, as an import leaves it out. */\nexport function siteImportLeftOutReason(reason: SiteImportSkipReason): AiSiteImportLeftOutReason {\n switch (reason) {\n case 'login-form':\n return 'login-form'\n case 'robots':\n return 'robots'\n case 'too-large':\n return 'too-large'\n case 'off-origin-redirect':\n return 'off-site'\n default:\n return 'unreadable'\n }\n}\n"],"names":["neutralizeUntrustedText","AI_SITE_IMPORT_IMAGES_PER_PAGE","AI_SITE_IMPORT_LABEL_MAX_CHARS","AI_SITE_IMPORT_MAX_SECTIONS","AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS","siteImportLabel","text","max","line","replace","trim","length","slice","trimEnd","wordsIn","match","siteImportPageWords","page","blocks","reduce","total","block","kind","TITLE_SEPARATOR","siteImportPageTitle","path","own","title","split","find","siteImportListedPage","url","foundBy","words","images","siteImportPageImages","seen","Set","add","alt","has","push","region","image","siteImportPageSections","headings","main","filter","opening","level","sections","toLowerCase","heading","name","siteImportPageText","lines","description","where","repeat","join","siteImportPageRead","at","siteImportLeftOutReason","reason"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type AiSiteImportState } from '../model/ai-site-import';
18
+ import type { AiJobItemLedger } from '../model/ai-jobs.types';
19
+ /** The abuse queue's `source` for a site import's row. */
20
+ export declare const SITE_IMPORT_REVIEW_SOURCE = "site-import";
21
+ /** One row per import job. */
22
+ export declare function siteImportReviewId(jobId: string): string;
23
+ /** The reference staff quote: `SI-` and the job's id. */
24
+ export declare function siteImportReviewReference(jobId: string): string;
25
+ export type SiteImportReviewEvent = 'read' | 'wave-started' | 'wave-finished';
26
+ export interface SiteImportReviewInput {
27
+ orgId: string;
28
+ hostId: string;
29
+ jobId: string;
30
+ state: AiSiteImportState;
31
+ items: readonly AiJobItemLedger[];
32
+ event: SiteImportReviewEvent;
33
+ /** The wave the event is about; absent on `read`. */
34
+ wave?: number;
35
+ nowMs: number;
36
+ }
37
+ /** The row's details: what was proved, read, kept, left out and brought in, rebuilt whole each time. */
38
+ export declare function siteImportReviewDetails(input: SiteImportReviewInput): string;
39
+ /** Writes (or rewrites) the import's row in the abuse queue. Never throws. */
40
+ export declare function fileSiteImportReview(firestore: FirebaseFirestore.Firestore, input: SiteImportReviewInput): Promise<string | null>;