@hraness/kb 0.17.1

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 (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,927 @@
1
+ // @bun
2
+ import {
3
+ CONTENT_REWRITE_TRUNCATION_WARNING,
4
+ articleMetadataLimits,
5
+ classifyPlatformUrl,
6
+ resolveRemote,
7
+ rewriteContentWithStatus,
8
+ scanImageSources
9
+ } from "./index-hgve9rh2.js";
10
+ import {
11
+ FetchFailure,
12
+ safeFetch
13
+ } from "./index-e5fbsywq.js";
14
+
15
+ // src/clip/assets.ts
16
+ import { createHash } from "crypto";
17
+ import { mkdirSync, writeFileSync } from "fs";
18
+ import { join } from "path";
19
+ function startsWith(bytes, signature) {
20
+ return signature.every((byte, index) => bytes[index] === byte);
21
+ }
22
+ function ascii(bytes, start, length) {
23
+ return new TextDecoder().decode(bytes.slice(start, start + length));
24
+ }
25
+ function sniffImage(bytes) {
26
+ if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) {
27
+ return { mimeType: "image/png", extension: "png" };
28
+ }
29
+ if (startsWith(bytes, [255, 216, 255]))
30
+ return { mimeType: "image/jpeg", extension: "jpg" };
31
+ const prefix = ascii(bytes, 0, 6);
32
+ if (prefix === "GIF87a" || prefix === "GIF89a")
33
+ return { mimeType: "image/gif", extension: "gif" };
34
+ if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP") {
35
+ return { mimeType: "image/webp", extension: "webp" };
36
+ }
37
+ if (ascii(bytes, 4, 4) === "ftyp") {
38
+ const brand = ascii(bytes, 8, 4);
39
+ if (brand === "avif" || brand === "avis")
40
+ return { mimeType: "image/avif", extension: "avif" };
41
+ }
42
+ return null;
43
+ }
44
+ function sha256(bytes) {
45
+ return createHash("sha256").update(bytes).digest("hex");
46
+ }
47
+ function inertAssetUrl(url) {
48
+ const inert = new URL(url);
49
+ inert.username = "";
50
+ inert.password = "";
51
+ inert.search = "";
52
+ inert.hash = "";
53
+ return inert.href;
54
+ }
55
+ function safeAssetFailure(error) {
56
+ if (!(error instanceof FetchFailure))
57
+ return "image request failed";
58
+ switch (error.code) {
59
+ case "private-network":
60
+ return "image request was blocked by the private-network boundary";
61
+ case "timeout":
62
+ return "image request timed out";
63
+ case "too-large":
64
+ return "image response exceeded its byte limit";
65
+ case "redirect":
66
+ return "image redirect chain was rejected";
67
+ case "dns":
68
+ return "image hostname could not be resolved";
69
+ case "http":
70
+ return "image server returned an unsuccessful response";
71
+ case "invalid-url":
72
+ return "image URL was rejected";
73
+ case "network":
74
+ return "image request failed";
75
+ }
76
+ }
77
+ async function downloadImage(source, options, maxBytes) {
78
+ const remote = resolveRemote(source, options.baseUrl);
79
+ if (remote === null)
80
+ return { ok: false, source, warning: "Skipped a non-web image target.", networkBytes: 0 };
81
+ let authenticationWarning = null;
82
+ let cookieHeader;
83
+ if (options.cookieHeaderProvider !== undefined) {
84
+ try {
85
+ cookieHeader = await options.cookieHeaderProvider(remote) ?? undefined;
86
+ } catch {
87
+ authenticationWarning = "The explicitly selected cookie source could not provide origin-scoped image cookies.";
88
+ }
89
+ }
90
+ try {
91
+ const response = await (options.fetchResource ?? safeFetch)(remote, {
92
+ timeoutMs: options.timeoutMs,
93
+ maxBytes,
94
+ allowPrivateNetwork: options.allowPrivateNetwork,
95
+ userAgent: options.userAgent,
96
+ referer: options.baseUrl.href,
97
+ accept: "image/avif,image/webp,image/png,image/jpeg,image/gif;q=0.9,*/*;q=0.1",
98
+ ...cookieHeader === undefined ? {} : { cookieHeader },
99
+ retries: 0
100
+ });
101
+ const image = sniffImage(response.bytes);
102
+ if (image === null) {
103
+ const declared = response.contentType?.split(";")[0]?.trim() ?? "unknown content type";
104
+ return {
105
+ ok: false,
106
+ source,
107
+ warning: `${authenticationWarning === null ? "" : `${authenticationWarning} `}Kept remote ${inertAssetUrl(remote)}: response was not a supported raster image (${declared})`,
108
+ networkBytes: response.bytes.byteLength
109
+ };
110
+ }
111
+ return { ok: true, source, url: response.finalUrl, bytes: response.bytes, image, networkBytes: response.bytes.byteLength };
112
+ } catch (error) {
113
+ return {
114
+ ok: false,
115
+ source,
116
+ warning: `${authenticationWarning === null ? "" : `${authenticationWarning} `}Kept remote ${inertAssetUrl(remote)}: ${safeAssetFailure(error)}`,
117
+ networkBytes: maxBytes
118
+ };
119
+ }
120
+ }
121
+ async function localizeAssets(content, options) {
122
+ const maxSources = Math.max(1, Math.min(options.maxSources ?? 1000, 1e4));
123
+ const discovery = scanImageSources(content, maxSources + 1);
124
+ const discoveredSources = [...discovery.sources].sort((left, right) => left.localeCompare(right));
125
+ const sources = discoveredSources.slice(0, maxSources);
126
+ const warnings = discoveredSources.length > sources.length ? [`Image localization stopped at ${sources.length} sources; ${discovery.truncated ? "at least " : ""}${discoveredSources.length - sources.length} additional remote image(s) remain inert links.`] : [];
127
+ if (discovery.truncated) {
128
+ warnings.push("Image discovery reached a safety limit; additional or over-limit image candidates remain inert.");
129
+ }
130
+ const rewrittenResult = (localBySource2) => {
131
+ const rewritten2 = rewriteContentWithStatus(content, options.baseUrl, localBySource2, {
132
+ maxImageSources: maxSources + 1
133
+ });
134
+ if (rewritten2.truncated)
135
+ warnings.push(CONTENT_REWRITE_TRUNCATION_WARNING);
136
+ return rewritten2;
137
+ };
138
+ if (discovery.requiresInertFallback) {
139
+ const rewritten2 = rewrittenResult(new Map);
140
+ return {
141
+ ...rewritten2,
142
+ assets: [],
143
+ warnings
144
+ };
145
+ }
146
+ if (discoveredSources.length === 0) {
147
+ const rewritten2 = rewrittenResult(new Map);
148
+ return {
149
+ ...rewritten2,
150
+ assets: [],
151
+ warnings
152
+ };
153
+ }
154
+ mkdirSync(options.assetsDirectory, { recursive: true });
155
+ const results = new Map;
156
+ const workerCount = Math.max(1, Math.min(options.concurrency ?? 4, sources.length, 16));
157
+ let remainingNetworkBytes = options.maxTotalAssetBytes;
158
+ const deadline = Date.now() + options.timeoutMs;
159
+ for (let cursor = 0;cursor < sources.length && remainingNetworkBytes > 0; ) {
160
+ const remainingTime = deadline - Date.now();
161
+ if (remainingTime <= 0)
162
+ break;
163
+ const batchSize = Math.min(workerCount, sources.length - cursor, remainingNetworkBytes);
164
+ const allocation = Math.min(options.maxAssetBytes, Math.floor(remainingNetworkBytes / batchSize));
165
+ const batch = sources.slice(cursor, cursor + batchSize);
166
+ cursor += batch.length;
167
+ remainingNetworkBytes -= allocation * batch.length;
168
+ const downloaded = await Promise.all(batch.map((source) => downloadImage(source, { ...options, timeoutMs: Math.max(1, Math.min(options.timeoutMs, remainingTime)) }, allocation)));
169
+ for (const result of downloaded) {
170
+ results.set(result.source, result);
171
+ remainingNetworkBytes += Math.max(0, allocation - result.networkBytes);
172
+ }
173
+ }
174
+ const localBySource = new Map;
175
+ const assetsByHash = new Map;
176
+ let totalBytes = 0;
177
+ let unattempted = 0;
178
+ for (const source of sources) {
179
+ const result = results.get(source);
180
+ if (result === undefined) {
181
+ unattempted += 1;
182
+ continue;
183
+ }
184
+ if (!result.ok) {
185
+ warnings.push(result.warning);
186
+ continue;
187
+ }
188
+ const digest = sha256(result.bytes);
189
+ const existing = assetsByHash.get(digest);
190
+ if (existing !== undefined) {
191
+ localBySource.set(source, existing.path);
192
+ continue;
193
+ }
194
+ if (totalBytes + result.bytes.byteLength > options.maxTotalAssetBytes) {
195
+ warnings.push(`Kept remote ${inertAssetUrl(result.url)}: total asset limit ${options.maxTotalAssetBytes} bytes would be exceeded`);
196
+ continue;
197
+ }
198
+ const filename = `${digest}.${result.image.extension}`;
199
+ const relativePath = `assets/${filename}`;
200
+ writeFileSync(join(options.assetsDirectory, filename), result.bytes, { mode: 420 });
201
+ totalBytes += result.bytes.byteLength;
202
+ const record = {
203
+ source: (() => {
204
+ const resolved = resolveRemote(source, options.baseUrl);
205
+ return resolved === null ? source : inertAssetUrl(resolved);
206
+ })(),
207
+ url: inertAssetUrl(result.url),
208
+ path: relativePath,
209
+ mimeType: result.image.mimeType,
210
+ bytes: result.bytes.byteLength,
211
+ sha256: digest
212
+ };
213
+ assetsByHash.set(digest, record);
214
+ localBySource.set(source, relativePath);
215
+ }
216
+ if (unattempted > 0) {
217
+ const reason = Date.now() >= deadline ? "total asset deadline" : "aggregate asset network-byte budget";
218
+ warnings.push(`${unattempted} remote image source(s) were not requested because the ${reason} was exhausted.`);
219
+ }
220
+ const rewritten = rewrittenResult(localBySource);
221
+ return {
222
+ ...rewritten,
223
+ assets: [...assetsByHash.values()].sort((left, right) => left.path.localeCompare(right.path)),
224
+ warnings
225
+ };
226
+ }
227
+
228
+ // src/clip/extract.ts
229
+ var nonEmpty = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
230
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
231
+ var MAX_RENDERED_PAGE_FALLBACK_BYTES = 256 * 1024;
232
+ var renderedPageTruncationMarker = "[Rendered page text truncated at the bounded fallback limit.]";
233
+ function isWhitespaceCodeUnit(code) {
234
+ return code >= 9 && code <= 13 || code === 32 || code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
235
+ }
236
+ function utf8CodePointWidth(value, index) {
237
+ const first = value.charCodeAt(index);
238
+ if (first <= 127)
239
+ return { bytes: 1, codeUnits: 1 };
240
+ if (first <= 2047)
241
+ return { bytes: 2, codeUnits: 1 };
242
+ const second = value.charCodeAt(index + 1);
243
+ if (first >= 55296 && first <= 56319 && second >= 56320 && second <= 57343) {
244
+ return { bytes: 4, codeUnits: 2 };
245
+ }
246
+ return { bytes: 3, codeUnits: 1 };
247
+ }
248
+ function utf8PrefixEnd(value, maxBytes) {
249
+ let bytes = 0;
250
+ let index = 0;
251
+ while (index < value.length) {
252
+ const width = utf8CodePointWidth(value, index);
253
+ if (bytes + width.bytes > maxBytes)
254
+ break;
255
+ bytes += width.bytes;
256
+ index += width.codeUnits;
257
+ }
258
+ return index;
259
+ }
260
+ function boundedRenderedPageText(value, requestedByteLimit) {
261
+ if (typeof value !== "string")
262
+ return null;
263
+ const byteLimit = typeof requestedByteLimit === "number" && Number.isSafeInteger(requestedByteLimit) && requestedByteLimit > 0 ? Math.min(requestedByteLimit, MAX_RENDERED_PAGE_FALLBACK_BYTES) : MAX_RENDERED_PAGE_FALLBACK_BYTES;
264
+ const fullEnd = utf8PrefixEnd(value, byteLimit);
265
+ if (fullEnd === value.length) {
266
+ const content = value.trim();
267
+ return content === "" ? null : { content, truncated: false, byteLimit };
268
+ }
269
+ const detailedMarker = `
270
+
271
+ ${renderedPageTruncationMarker}
272
+ `;
273
+ const detailedMarkerBytes = new TextEncoder().encode(detailedMarker).byteLength;
274
+ const marker = detailedMarkerBytes < byteLimit ? detailedMarker : byteLimit >= 3 ? "\u2026" : ".".repeat(byteLimit);
275
+ const markerBytes = new TextEncoder().encode(marker).byteLength;
276
+ const boundedEnd = utf8PrefixEnd(value, byteLimit - markerBytes);
277
+ const prefix = value.slice(0, boundedEnd).trim();
278
+ if (prefix === "")
279
+ return null;
280
+ return { content: `${prefix}${marker}`, truncated: true, byteLimit };
281
+ }
282
+ function boundedTrimmedSlice(value, start, end, maxCodeUnits) {
283
+ while (start < end && isWhitespaceCodeUnit(value.charCodeAt(start)))
284
+ start += 1;
285
+ while (end > start && isWhitespaceCodeUnit(value.charCodeAt(end - 1)))
286
+ end -= 1;
287
+ if (start === end)
288
+ return null;
289
+ if (end - start <= maxCodeUnits)
290
+ return value.slice(start, end);
291
+ let boundedEnd = start + Math.max(0, maxCodeUnits - 1);
292
+ const finalCode = value.charCodeAt(boundedEnd - 1);
293
+ if (finalCode >= 55296 && finalCode <= 56319)
294
+ boundedEnd -= 1;
295
+ return `${value.slice(start, boundedEnd)}\u2026`;
296
+ }
297
+ function boundedMetadata(value, maxCodeUnits) {
298
+ return typeof value === "string" ? boundedTrimmedSlice(value, 0, value.length, maxCodeUnits) : null;
299
+ }
300
+ function countWords(value) {
301
+ let count = 0;
302
+ let insideWord = false;
303
+ for (let index = 0;index < value.length; index += 1) {
304
+ if (isWhitespaceCodeUnit(value.charCodeAt(index))) {
305
+ insideWord = false;
306
+ } else if (!insideWord) {
307
+ count += 1;
308
+ insideWord = true;
309
+ }
310
+ }
311
+ return count;
312
+ }
313
+ function isAsciiWordCodeUnit(code) {
314
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
315
+ }
316
+ function asciiCaseEqualAt(value, offset, expected, end = value.length) {
317
+ if (offset < 0 || offset + expected.length > end)
318
+ return false;
319
+ for (let index = 0;index < expected.length; index += 1) {
320
+ const actual = value.charCodeAt(offset + index);
321
+ const folded = actual >= 65 && actual <= 90 ? actual + 32 : actual;
322
+ if (folded !== expected.charCodeAt(index))
323
+ return false;
324
+ }
325
+ return true;
326
+ }
327
+ function tagHasExactCommentClass(html, start, end) {
328
+ const doubleQuoted = 'class="comment"';
329
+ const singleQuoted = "class='comment'";
330
+ for (let index = start;index < end; index += 1) {
331
+ const preceding = index === 0 ? -1 : html.charCodeAt(index - 1);
332
+ if (preceding >= 0 && isAsciiWordCodeUnit(preceding))
333
+ continue;
334
+ if (asciiCaseEqualAt(html, index, doubleQuoted, end) || asciiCaseEqualAt(html, index, singleQuoted, end))
335
+ return true;
336
+ }
337
+ return false;
338
+ }
339
+ function countDefuddleCommentMarkers(html) {
340
+ let count = 0;
341
+ let cursor = 0;
342
+ while (cursor < html.length) {
343
+ const start = html.indexOf("<", cursor);
344
+ if (start < 0)
345
+ break;
346
+ cursor = start + 1;
347
+ if (!asciiCaseEqualAt(html, start + 1, "div"))
348
+ continue;
349
+ const afterName = start + 4;
350
+ if (afterName < html.length && isAsciiWordCodeUnit(html.charCodeAt(afterName)))
351
+ continue;
352
+ const end = html.indexOf(">", afterName);
353
+ if (end < 0)
354
+ break;
355
+ cursor = end + 1;
356
+ if (tagHasExactCommentClass(html, afterName, end))
357
+ count += 1;
358
+ }
359
+ return count;
360
+ }
361
+ function countDefuddleSeparators(html) {
362
+ let count = 0;
363
+ let cursor = 0;
364
+ while (cursor < html.length) {
365
+ const start = html.indexOf("<", cursor);
366
+ if (start < 0)
367
+ break;
368
+ cursor = start + 1;
369
+ if (!asciiCaseEqualAt(html, start + 1, "hr"))
370
+ continue;
371
+ const afterName = start + 3;
372
+ const next = html.charCodeAt(afterName);
373
+ if (next === 62) {
374
+ count += 1;
375
+ cursor = afterName + 1;
376
+ continue;
377
+ }
378
+ if (!isWhitespaceCodeUnit(next))
379
+ continue;
380
+ const end = html.indexOf(">", afterName + 1);
381
+ if (end < 0)
382
+ break;
383
+ count += 1;
384
+ cursor = end + 1;
385
+ }
386
+ return count;
387
+ }
388
+ function countMarkdownMarkers(value, kind, limit) {
389
+ const prefix = kind === "image" ? "![" : "[";
390
+ let count = 0;
391
+ let cursor = 0;
392
+ while (count < limit && cursor < value.length) {
393
+ const start = value.indexOf(prefix, cursor);
394
+ if (start < 0)
395
+ break;
396
+ const openBracket = kind === "image" ? start + 1 : start;
397
+ const closeBracket = value.indexOf("]", openBracket + 1);
398
+ if (closeBracket < 0)
399
+ break;
400
+ const nonEmptyLinkLabel = kind === "image" || closeBracket > openBracket + 1;
401
+ if (nonEmptyLinkLabel && value.charCodeAt(closeBracket + 1) === 40) {
402
+ count += 1;
403
+ cursor = closeBracket + 2;
404
+ } else {
405
+ cursor = start + prefix.length;
406
+ }
407
+ }
408
+ return count;
409
+ }
410
+ function defuddleWorkerUrl(moduleUrl = import.meta.url) {
411
+ return moduleUrl.endsWith(".ts") ? new URL("./defuddle-worker.ts", moduleUrl) : new URL("./clip/defuddle-worker.js", moduleUrl);
412
+ }
413
+ async function runDefuddleWorker(acquisition, scope, timeoutMs) {
414
+ const worker = new Worker(defuddleWorkerUrl().href, { type: "module" });
415
+ let timeout;
416
+ try {
417
+ const result = await new Promise((resolve, reject) => {
418
+ timeout = setTimeout(() => {
419
+ reject(new Error(`Defuddle exceeded the ${timeoutMs}ms extraction deadline.`));
420
+ }, timeoutMs);
421
+ worker.onmessage = (event) => {
422
+ const message = event.data;
423
+ if (!isRecord(message) || typeof message.ok !== "boolean") {
424
+ reject(new Error("Defuddle worker returned malformed data."));
425
+ return;
426
+ }
427
+ if (message.ok === true && isRecord(message.value)) {
428
+ resolve({ ok: true, value: message.value });
429
+ return;
430
+ }
431
+ resolve({
432
+ ok: false,
433
+ message: typeof message.message === "string" ? message.message.slice(0, 1000) : "Defuddle worker failed."
434
+ });
435
+ };
436
+ worker.onerror = () => reject(new Error("Defuddle worker failed."));
437
+ worker.postMessage({
438
+ html: acquisition.body,
439
+ url: acquisition.finalUrl.href,
440
+ includeReplies: scope === "page" ? false : scope === "comments" ? true : "extractors"
441
+ });
442
+ });
443
+ if (!result.ok)
444
+ throw new Error(result.message);
445
+ return result.value;
446
+ } finally {
447
+ if (timeout !== undefined)
448
+ clearTimeout(timeout);
449
+ worker.terminate();
450
+ }
451
+ }
452
+ function detectPlatform(url) {
453
+ return classifyPlatformUrl(url.href)?.platform ?? "generic";
454
+ }
455
+ function detectedExtractorPlatform(value, fallback) {
456
+ if (fallback !== "generic")
457
+ return fallback;
458
+ return value === "github" ? "github" : value === "discourse" ? "discourse" : fallback;
459
+ }
460
+ var trackingKeys = new Set([
461
+ "fbclid",
462
+ "gclid",
463
+ "dclid",
464
+ "msclkid",
465
+ "mc_cid",
466
+ "mc_eid",
467
+ "igshid",
468
+ "mibextid"
469
+ ]);
470
+ var credentialQueryKey = /(?:^|[-_])(?:access[-_]?token|refresh[-_]?token|auth(?:orization)?|api[-_]?key|credential|csrf|xsrf|jwt|pass(?:word|wd)?|secret|session[-_]?id|signature|sig|code|ticket|otp|nonce|key|magic[-_]?link|one[-_]?time)(?:$|[-_])/i;
471
+ function canonicalizeUrl(url, platform = detectPlatform(url)) {
472
+ const canonical = new URL(url);
473
+ canonical.hash = "";
474
+ for (const key of [...canonical.searchParams.keys()]) {
475
+ if (key.toLowerCase().startsWith("utm_") || trackingKeys.has(key.toLowerCase()) || credentialQueryKey.test(key)) {
476
+ canonical.searchParams.delete(key);
477
+ }
478
+ }
479
+ if (platform === "x") {
480
+ canonical.hostname = "x.com";
481
+ canonical.searchParams.delete("s");
482
+ canonical.searchParams.delete("t");
483
+ }
484
+ return canonical;
485
+ }
486
+ var MAX_SCHEMA_COMMENT_NODES = 50000;
487
+ function schemaCommentCount(value) {
488
+ const seen = new Set;
489
+ const stack = [value];
490
+ let visited = 0;
491
+ while (stack.length > 0 && visited < MAX_SCHEMA_COMMENT_NODES) {
492
+ const current = stack.pop();
493
+ visited += 1;
494
+ if (typeof current !== "object" || current === null || seen.has(current))
495
+ continue;
496
+ seen.add(current);
497
+ if (Array.isArray(current)) {
498
+ const remaining2 = MAX_SCHEMA_COMMENT_NODES - visited;
499
+ for (let index = Math.min(current.length, remaining2) - 1;index >= 0; index -= 1) {
500
+ stack.push(current[index]);
501
+ }
502
+ continue;
503
+ }
504
+ if (!isRecord(current))
505
+ continue;
506
+ const own = current.commentCount;
507
+ if (typeof own === "number" && Number.isSafeInteger(own) && own >= 0)
508
+ return own;
509
+ if (typeof own === "string" && /^\d+$/.test(own)) {
510
+ const parsed = Number(own);
511
+ if (Number.isSafeInteger(parsed))
512
+ return parsed;
513
+ }
514
+ const children = [];
515
+ const remaining = MAX_SCHEMA_COMMENT_NODES - visited;
516
+ if (remaining <= 0)
517
+ continue;
518
+ for (const key in current) {
519
+ if (!Object.prototype.hasOwnProperty.call(current, key))
520
+ continue;
521
+ children.push(current[key]);
522
+ if (children.length >= remaining)
523
+ break;
524
+ }
525
+ for (let index = children.length - 1;index >= 0; index -= 1)
526
+ stack.push(children[index]);
527
+ }
528
+ return null;
529
+ }
530
+ function countDefuddleConversationItems(response, platform) {
531
+ const html = nonEmpty(response.content);
532
+ const extractorType = nonEmpty(response.extractorType);
533
+ if (html === null || extractorType === null)
534
+ return null;
535
+ const supported = new Set(["twitter", "reddit", "hackernews", "github", "discourse", "linkedin"]);
536
+ if (!supported.has(extractorType))
537
+ return null;
538
+ const comments = countDefuddleCommentMarkers(html);
539
+ if (platform !== "x" || extractorType !== "twitter")
540
+ return comments;
541
+ const separators = countDefuddleSeparators(html);
542
+ return comments + Math.max(0, separators - (comments > 0 ? 1 : 0));
543
+ }
544
+ function restoreXPostLineBreaks(content, description) {
545
+ if (description === null || !/[\r\n]/.test(description))
546
+ return content;
547
+ const preserved = description.replace(/\r\n?/g, `
548
+ `).trim();
549
+ const flattened = preserved.replace(/\s+/g, " ");
550
+ const offset = content.indexOf(flattened);
551
+ if (offset < 0)
552
+ return content;
553
+ const literal = preserved.split(`
554
+ `).map((line) => {
555
+ let escaped = line.replace(/\\/g, "\\\\").replace(/([`*_~[\]])/g, "\\$1").replace(/</g, "&lt;").replace(/>/g, "&gt;");
556
+ escaped = escaped.replace(/^(\s*)(#{1,6}|[-+]|\d+[.)])(?=\s)/, "$1\\$2");
557
+ if (/^\s*-{3,}\s*$/.test(escaped))
558
+ escaped = escaped.replace("-", "\\-");
559
+ if (/^(?: {4}|\t)/.test(escaped))
560
+ escaped = `&#32;${escaped.slice(1)}`;
561
+ return escaped;
562
+ }).join(`
563
+ `);
564
+ return `${content.slice(0, offset)}${literal}${content.slice(offset + flattened.length)}`;
565
+ }
566
+ function normalizedDefuddleMediaUrl(value, baseUrl) {
567
+ const candidate = nonEmpty(value);
568
+ if (candidate === null || candidate.length > 8192)
569
+ return null;
570
+ const resolved = resolveRemote(candidate, baseUrl);
571
+ if (resolved === null || resolved.username !== "" || resolved.password !== "")
572
+ return null;
573
+ resolved.hash = "";
574
+ return resolved;
575
+ }
576
+ var defuddleInlineImage = /!\[([^\]\r\n]*)\]\((?:<([^<>\r\n]*)>|([^()\s\r\n]+))((?:\s+"[^"\r\n]*")?)\)/g;
577
+ function removeDefuddleSamePageImages(content, reportedImage, baseUrl) {
578
+ const exactCandidates = new Set;
579
+ const exactPage = new URL(baseUrl);
580
+ exactPage.hash = "";
581
+ exactCandidates.add(exactPage.href);
582
+ const reported = normalizedDefuddleMediaUrl(reportedImage, baseUrl);
583
+ if (reported !== null && reported.origin === baseUrl.origin && reported.pathname === baseUrl.pathname)
584
+ exactCandidates.add(reported.href);
585
+ return content.replace(defuddleInlineImage, (whole, _alt, bracketed, bare) => {
586
+ const image = normalizedDefuddleMediaUrl(bracketed ?? bare, baseUrl);
587
+ return image !== null && exactCandidates.has(image.href) ? "" : whole;
588
+ });
589
+ }
590
+ function retainDefuddleMedia(content, response, baseUrl) {
591
+ content = removeDefuddleSamePageImages(content, response.image, baseUrl);
592
+ const existing = new Set;
593
+ const scan = scanImageSources(content);
594
+ for (const source of scan.sources) {
595
+ const url = normalizedDefuddleMediaUrl(source, baseUrl);
596
+ if (url !== null)
597
+ existing.add(url.href);
598
+ }
599
+ const candidates = [
600
+ { value: response.image, label: "Cover image" }
601
+ ];
602
+ if (Array.isArray(response.captureVideoPosters)) {
603
+ const maximum = Math.min(response.captureVideoPosters.length, 64);
604
+ for (let index = 0;index < maximum; index += 1) {
605
+ candidates.push({ value: response.captureVideoPosters[index], label: "Video thumbnail" });
606
+ }
607
+ }
608
+ const additions = [];
609
+ for (const candidate of candidates) {
610
+ const url = normalizedDefuddleMediaUrl(candidate.value, baseUrl);
611
+ if (url === null || existing.has(url.href) || url.origin === baseUrl.origin && url.pathname === baseUrl.pathname)
612
+ continue;
613
+ existing.add(url.href);
614
+ additions.push(`![${candidate.label}](<${url.href}>)`);
615
+ }
616
+ return additions.length === 0 ? content : `${content.trimEnd()}
617
+
618
+ ${additions.join(`
619
+
620
+ `)}
621
+ `;
622
+ }
623
+ function compactCount(value, suffix) {
624
+ const number = Number(value.replace(/,/g, ""));
625
+ if (!Number.isFinite(number) || number < 0)
626
+ return null;
627
+ const multiplier = suffix?.toLowerCase() === "m" ? 1e6 : suffix?.toLowerCase() === "k" ? 1000 : 1;
628
+ const result = Math.floor(number * multiplier);
629
+ return Number.isSafeInteger(result) ? result : null;
630
+ }
631
+ function visibleCommentCount(content, platform) {
632
+ const patterns = platform === "x" ? [/\bread\s+([\d,.]+)\s*([km])?\s+repl(?:y|ies)\b/i, /\b([\d,.]+)\s*([km])?\s+repl(?:y|ies)\b/i] : [/\b(?:view|read|show)\s+(?:all\s+)?([\d,.]+)\s*([km])?\s+comments?\b/i];
633
+ for (const pattern of patterns) {
634
+ const match = pattern.exec(content);
635
+ if (match?.[1] === undefined)
636
+ continue;
637
+ const count = compactCount(match[1], match[2]);
638
+ if (count !== null)
639
+ return count;
640
+ }
641
+ return null;
642
+ }
643
+ var blockedPattern = /(?:verify (?:that )?you are (?:a )?human|(?:complete|solve) (?:the )?captcha|\bcaptcha\b|access denied|request (?:has been )?blocked|unusual traffic|cloudflare ray id)/i;
644
+ var blockedTitlePattern = /^(?:403(?: forbidden)?|access denied|request blocked|unusual traffic|verify (?:that )?you are (?:a )?human|human verification|captcha|security (?:check|verification)|attention required|just a moment(?:\.{3})?)$/i;
645
+ var blockedContextPattern = /(?:\b(?:please )?(?:verify|confirm) (?:that )?you are (?:a )?human\b|\b(?:complete|solve) (?:the )?captcha\b|\b(?:you (?:do not|don't) have permission|you have been blocked)\b|\b(?:your|this) (?:request|access|ip(?: address)?) (?:has been|was|is) blocked\b|\bunusual traffic from your (?:computer )?network\b|\bautomated (?:queries|requests)\b|\b(?:before proceeding|to continue),? (?:please )?(?:verify|complete|enable)\b|\bcloudflare ray id\b)/i;
646
+ var blockedStandaloneLinePattern = /(?:^|\n)[ \t]*(?:#{1,6}[ \t]+)?(?:403(?: forbidden)?|access denied|request blocked|unusual traffic|verify (?:that )?you are (?:a )?human|captcha|security (?:check|verification))[.!]?[ \t]*(?:\r?\n|$)/i;
647
+ var rateLimitSignalPattern = /\b(?:429(?: too many requests)?|too many requests|rate limit(?:ed| exceeded)?)\b/i;
648
+ var rateLimitTitlePattern = /^(?:429(?: too many requests)?|too many requests|rate limit(?:ed| exceeded)?)$/i;
649
+ var rateLimitStandaloneLinePattern = /(?:^|\n)[ \t]*(?:#{1,6}[ \t]+)?(?:429(?: too many requests)?|too many requests|rate limit(?:ed| exceeded)?)[.!]?[ \t]*(?:\r?\n|$)/i;
650
+ var rateLimitRetryContextPattern = /\b(?:please )?(?:(?:try|retry) again(?: later| in \d+)|wait (?:a moment|before retrying)|respect the retry-after header)\b/i;
651
+ var articleDiscussionPattern = /(?:\bhow to\b|\btroubleshoot(?:ing)?\b|\b(?:this|the) (?:article|guide|tutorial)\b|\b(?:this|the) (?:article|guide|tutorial) explains?\b|\blearn (?:how|why)\b)/i;
652
+ var MAX_BLOCKED_SHELL_CODE_UNITS = 4096;
653
+ var MAX_BLOCKED_SHELL_WORDS = 160;
654
+ var MAX_STANDALONE_BLOCKED_SHELL_WORDS = 24;
655
+ function looksLikeBlockedShell(content, title) {
656
+ if (content.length > MAX_BLOCKED_SHELL_CODE_UNITS)
657
+ return false;
658
+ const wordCount = countWords(content);
659
+ if (wordCount > MAX_BLOCKED_SHELL_WORDS)
660
+ return false;
661
+ const normalizedTitle = (title ?? "").slice(0, articleMetadataLimits.title).replace(/\s+/g, " ").trim();
662
+ const boundedVisible = `${normalizedTitle}
663
+ ${content}`;
664
+ if (articleDiscussionPattern.test(boundedVisible))
665
+ return false;
666
+ if (wordCount <= MAX_STANDALONE_BLOCKED_SHELL_WORDS && (blockedStandaloneLinePattern.test(content) || rateLimitStandaloneLinePattern.test(content)))
667
+ return true;
668
+ const exactGateTitle = blockedTitlePattern.test(normalizedTitle) || rateLimitTitlePattern.test(normalizedTitle);
669
+ const rateLimitShell = rateLimitSignalPattern.test(content) && rateLimitRetryContextPattern.test(content);
670
+ const hasBlockSignal = exactGateTitle || blockedPattern.test(content) || rateLimitShell;
671
+ return hasBlockSignal && (exactGateTitle || blockedContextPattern.test(content) || rateLimitShell);
672
+ }
673
+ var authenticationGatePattern = /(?:\b(?:sign|log) in to (?:continue|read|view|see|access|comment|reply)\b|\blogin required\b|\bmembers? only\b|\bsubscriber-only\b|\bsubscribe to (?:continue|read)\b|\bthis content is private\b|\byou must be logged in\b)/i;
674
+ var paywallCallToActionPattern = /\b(?:subscribe(?: now)? to (?:unlock (?:this|the) (?:article|story|content)|(?:keep|continue) reading)|create (?:an? |your )?account to (?:keep|continue) reading|register to (?:keep|continue) reading|sign up to (?:keep|continue) reading)\b/i;
675
+ var paywallTitlePattern = /^(?:subscribe(?: now)? to (?:unlock (?:this|the) (?:article|story|content)|(?:keep|continue) reading)|create (?:an? |your )?account to (?:keep|continue) reading|register to (?:keep|continue) reading|sign up to (?:keep|continue) reading)[.!]?$/i;
676
+ var paywallStandaloneLinePattern = /(?:^|\n)[ \t]*(?:#{1,6}[ \t]+)?(?:subscribe(?: now)? to (?:unlock (?:this|the) (?:article|story|content)|(?:keep|continue) reading)|create (?:an? |your )?account to (?:keep|continue) reading|register to (?:keep|continue) reading|sign up to (?:keep|continue) reading)[.!]?[ \t]*(?:\r?\n|$)/i;
677
+ var paywallDiscussionPattern = /\b(?:analy[sz](?:e|es|ed|ing)|discuss(?:es|ed|ing)?|examin(?:e|es|ed|ing)|quot(?:e|es|ed|ing)|stud(?:y|ies|ied|ying)|interface copy|wording|publishers? (?:sometimes )?(?:say|write|use))\b/i;
678
+ var shellPattern = /(?:enable javascript|javascript is disabled|something went wrong|try reloading)/i;
679
+ var xReplyGatePattern = /\bjoin\s+x\s+now\s+to\s+read\s+repl(?:y|ies)\b/i;
680
+ var xCombinedAccountShellPattern = /\blog\s*in\s*sign\s*up\b/i;
681
+ var loginShellPattern = /\blog\s*in\b/i;
682
+ var signupShellPattern = /\bsign\s*up\b/i;
683
+ function looksLikePaywallShell(content, title) {
684
+ if (content.length > MAX_BLOCKED_SHELL_CODE_UNITS)
685
+ return false;
686
+ const wordCount = countWords(content);
687
+ if (wordCount > MAX_BLOCKED_SHELL_WORDS)
688
+ return false;
689
+ const normalizedTitle = (title ?? "").slice(0, articleMetadataLimits.title).replace(/\s+/g, " ").trim();
690
+ if (paywallTitlePattern.test(normalizedTitle) || paywallStandaloneLinePattern.test(content))
691
+ return true;
692
+ if (wordCount > 48)
693
+ return false;
694
+ const boundedVisible = `${normalizedTitle}
695
+ ${content}`;
696
+ return paywallCallToActionPattern.test(boundedVisible) && !paywallDiscussionPattern.test(boundedVisible);
697
+ }
698
+ function isRenderedConversationAccessGate(content, platform) {
699
+ if (authenticationGatePattern.test(content))
700
+ return true;
701
+ return platform === "x" && (xReplyGatePattern.test(content) || xCombinedAccountShellPattern.test(content) || loginShellPattern.test(content) && signupShellPattern.test(content));
702
+ }
703
+ function statusFor(content, title, scope, contentTruncated, renderedTextFallback) {
704
+ const visible = `${title ?? ""}
705
+ ${content}`;
706
+ if (looksLikeBlockedShell(content, title))
707
+ return "blocked";
708
+ const accessGate = authenticationGatePattern.test(visible) || looksLikePaywallShell(content, title);
709
+ if (accessGate && content.length < 1500)
710
+ return "auth-required";
711
+ if (shellPattern.test(visible) && content.length < 500)
712
+ return "unsupported";
713
+ if (content.trim().length < 40)
714
+ return "unsupported";
715
+ if (accessGate)
716
+ return "partial";
717
+ if (contentTruncated || renderedTextFallback)
718
+ return "partial";
719
+ if (scope === "thread" || scope === "comments")
720
+ return "partial";
721
+ return "complete";
722
+ }
723
+ function extractionShowsAccessControl(extraction) {
724
+ if (extraction.status === "auth-required" || extraction.status === "blocked")
725
+ return true;
726
+ const { content, title } = extraction.article;
727
+ return looksLikeBlockedShell(content, title) || looksLikePaywallShell(content, title) || authenticationGatePattern.test(`${title ?? ""}
728
+ ${content}`);
729
+ }
730
+ function scoreExtraction(article, status, wordCount, capturedItems, acquisition) {
731
+ const statusWeight = {
732
+ complete: 5000,
733
+ partial: 2000,
734
+ "auth-required": -2000,
735
+ blocked: -4000,
736
+ unsupported: -5000
737
+ };
738
+ const images = countMarkdownMarkers(article.content, "image", 100);
739
+ const links = countMarkdownMarkers(article.content, "link", 500);
740
+ const acquisitionAdjustment = acquisition.method.startsWith("browser") ? acquisition.contentType?.toLowerCase().includes("text/plain") === true ? -500 : 0 : 0;
741
+ return statusWeight[status] + Math.min(article.content.length, 50000) + Math.min(wordCount, 1e4) * 5 + Math.min(capturedItems, 1000) * 50 + images * 100 + links * 5 + acquisitionAdjustment;
742
+ }
743
+ function plainTextArticle(acquisition) {
744
+ const content = acquisition.body.trim();
745
+ if (content === "")
746
+ return null;
747
+ const browserTitle = boundedMetadata(acquisition.browserTitle, articleMetadataLimits.title);
748
+ const firstHeading = browserTitle === null ? firstMarkdownHeading(content) : null;
749
+ const pathname = acquisition.finalUrl.pathname;
750
+ let pathEnd = pathname.length;
751
+ while (pathEnd > 0 && pathname.charCodeAt(pathEnd - 1) === 47)
752
+ pathEnd -= 1;
753
+ const pathStart = pathname.lastIndexOf("/", pathEnd - 1) + 1;
754
+ const lastSegment = boundedTrimmedSlice(pathname, pathStart, pathEnd, articleMetadataLimits.title);
755
+ return {
756
+ content,
757
+ title: browserTitle ?? firstHeading ?? lastSegment ?? boundedMetadata(acquisition.finalUrl.hostname, articleMetadataLimits.title),
758
+ author: null,
759
+ published: null,
760
+ description: null
761
+ };
762
+ }
763
+ function firstMarkdownHeading(content) {
764
+ let lineStart = 0;
765
+ while (lineStart < content.length) {
766
+ const newline = content.indexOf(`
767
+ `, lineStart);
768
+ const lineEnd = newline < 0 ? content.length : newline;
769
+ let cursor = lineStart;
770
+ let hashes = 0;
771
+ while (hashes < 3 && cursor < lineEnd && content.charCodeAt(cursor) === 35) {
772
+ hashes += 1;
773
+ cursor += 1;
774
+ }
775
+ if (hashes > 0 && cursor < lineEnd && (content.charCodeAt(cursor) === 32 || content.charCodeAt(cursor) === 9)) {
776
+ const heading = boundedTrimmedSlice(content, cursor, lineEnd, articleMetadataLimits.title);
777
+ if (heading !== null)
778
+ return heading;
779
+ }
780
+ if (newline < 0)
781
+ break;
782
+ lineStart = newline + 1;
783
+ }
784
+ return null;
785
+ }
786
+ async function extractPage(acquisition, scope, timeoutMs = 30000) {
787
+ let platform = detectPlatform(acquisition.finalUrl);
788
+ const contentType = acquisition.contentType?.toLowerCase() ?? "";
789
+ let article = null;
790
+ let wordCount = 0;
791
+ let expectedItems = null;
792
+ let structurallyCapturedItems = null;
793
+ let extractor = "plain-text";
794
+ const warnings = [...acquisition.warnings];
795
+ const renderedPage = scope === "page" && acquisition.method.startsWith("browser") ? boundedRenderedPageText(acquisition.renderedText, acquisition.renderedTextByteLimit) : null;
796
+ let renderedPageFallback = false;
797
+ let renderedPageFallbackTruncated = false;
798
+ if (contentType.includes("text/markdown") || contentType.includes("text/plain")) {
799
+ article = plainTextArticle(acquisition);
800
+ wordCount = article === null ? 0 : countWords(article.content);
801
+ if (acquisition.method.startsWith("browser")) {
802
+ warnings.push("Rendered readable-text fallback may include surrounding account or interface content; review it before reuse.");
803
+ }
804
+ } else {
805
+ try {
806
+ const response = await runDefuddleWorker(acquisition, scope, timeoutMs);
807
+ platform = detectedExtractorPlatform(response.extractorType, platform);
808
+ const content = nonEmpty(response.contentMarkdown) ?? nonEmpty(response.content);
809
+ if (content !== null) {
810
+ const description = boundedMetadata(response.description, articleMetadataLimits.description);
811
+ const restoredContent = platform === "x" ? restoreXPostLineBreaks(content, description) : content;
812
+ article = {
813
+ content: retainDefuddleMedia(restoredContent, response, acquisition.finalUrl),
814
+ title: boundedMetadata(response.title, articleMetadataLimits.title) ?? boundedMetadata(acquisition.browserTitle, articleMetadataLimits.title),
815
+ author: boundedMetadata(response.author, articleMetadataLimits.author),
816
+ published: boundedMetadata(response.published, articleMetadataLimits.published),
817
+ description
818
+ };
819
+ wordCount = typeof response.wordCount === "number" && Number.isSafeInteger(response.wordCount) && response.wordCount >= 0 ? response.wordCount : countWords(content);
820
+ expectedItems = schemaCommentCount(response.schemaOrgData);
821
+ structurallyCapturedItems = countDefuddleConversationItems(response, platform);
822
+ extractor = platform === "generic" ? "defuddle" : `defuddle:${platform}`;
823
+ }
824
+ } catch (error) {
825
+ const message = error instanceof Error ? error.message : String(error);
826
+ if (renderedPage !== null) {
827
+ warnings.push("Article extraction failed; evaluated the bounded rendered-page text fallback from the same browser navigation.");
828
+ } else {
829
+ throw new Error(`Defuddle could not parse this acquisition: ${message}`, { cause: error });
830
+ }
831
+ }
832
+ }
833
+ if (renderedPage !== null) {
834
+ const primaryStatus = article === null ? "unsupported" : statusFor(article.content, article.title, scope, acquisition.contentTruncated === true, false);
835
+ const fallbackBase = plainTextArticle({
836
+ ...acquisition,
837
+ body: renderedPage.content,
838
+ contentType: "text/plain; charset=utf-8"
839
+ });
840
+ if (fallbackBase !== null) {
841
+ const fallbackStatus = statusFor(fallbackBase.content, fallbackBase.title, scope, renderedPage.truncated || acquisition.renderedTextTruncated === true, true);
842
+ if (article === null || primaryStatus === "unsupported" && fallbackStatus !== "unsupported") {
843
+ const primary = article;
844
+ article = {
845
+ ...fallbackBase,
846
+ title: primary?.title ?? fallbackBase.title,
847
+ author: primary?.author ?? fallbackBase.author,
848
+ published: primary?.published ?? fallbackBase.published,
849
+ description: primary?.description ?? fallbackBase.description
850
+ };
851
+ wordCount = countWords(article.content);
852
+ extractor = primary === null ? "rendered-page" : `${extractor}+rendered-page`;
853
+ renderedPageFallback = true;
854
+ renderedPageFallbackTruncated = renderedPage.truncated || acquisition.renderedTextTruncated === true;
855
+ warnings.push("Used bounded rendered-page text because article extraction produced no usable page body; it may include surrounding account or interface text and cannot prove feed completeness.");
856
+ if (renderedPageFallbackTruncated) {
857
+ warnings.push(`Rendered-page text reached the ${renderedPage.byteLimit}-byte fallback limit or the browser read boundary and was truncated.`);
858
+ }
859
+ }
860
+ }
861
+ }
862
+ if (article === null)
863
+ return null;
864
+ const renderedConversation = scope !== "page" && structurallyCapturedItems === null ? nonEmpty(acquisition.renderedText) : null;
865
+ if (renderedConversation !== null && renderedConversation !== article.content.trim()) {
866
+ if (isRenderedConversationAccessGate(renderedConversation, platform)) {
867
+ warnings.push("Skipped the separately rendered conversation context because it exposed an access gate rather than a trustworthy reply or comment tree.");
868
+ } else {
869
+ article = {
870
+ ...article,
871
+ content: `${article.content.trimEnd()}
872
+
873
+ ## Rendered conversation context
874
+
875
+ ${renderedConversation}
876
+ `
877
+ };
878
+ wordCount = countWords(article.content);
879
+ extractor = `${extractor}+rendered-context`;
880
+ warnings.push("Preserved the separately rendered conversation context because the article extractor exposed no trustworthy item tree; it can include duplicated article, account, or interface text.");
881
+ }
882
+ }
883
+ expectedItems ??= visibleCommentCount(article.content, platform);
884
+ const capturedItems = scope === "page" ? 1 : structurallyCapturedItems ?? 0;
885
+ if (scope === "page") {
886
+ expectedItems = null;
887
+ } else if (structurallyCapturedItems === null) {
888
+ warnings.push("The rendered response exposed no trustworthy per-item structure; capturedItems is conservatively reported as 0.");
889
+ } else if (expectedItems !== null && capturedItems > expectedItems) {
890
+ warnings.push(`The source declared ${expectedItems} scoped items, but ${capturedItems} items were observed; the expected count was normalized to the observed count.`);
891
+ expectedItems = capturedItems;
892
+ }
893
+ const status = statusFor(article.content, article.title, scope, renderedPageFallback ? renderedPageFallbackTruncated : acquisition.contentTruncated === true, renderedPageFallback || acquisition.method.startsWith("browser") && contentType.includes("text/plain"));
894
+ if (status !== "complete")
895
+ warnings.push(`Capture status is ${status}; inspect the source before relying on completeness.`);
896
+ const canonicalUrl = canonicalizeUrl(acquisition.finalUrl, platform);
897
+ return {
898
+ article,
899
+ canonicalUrl,
900
+ platform,
901
+ status,
902
+ score: scoreExtraction(article, status, wordCount, capturedItems, acquisition),
903
+ wordCount,
904
+ expectedItems,
905
+ capturedItems,
906
+ extractor,
907
+ warnings,
908
+ acquisition
909
+ };
910
+ }
911
+ function chooseBestExtraction(candidates) {
912
+ const statusRank = {
913
+ complete: 5,
914
+ partial: 4,
915
+ "auth-required": 3,
916
+ blocked: 2,
917
+ unsupported: 1
918
+ };
919
+ let best = null;
920
+ for (const candidate of candidates) {
921
+ if (best === null || statusRank[candidate.status] > statusRank[best.status] || statusRank[candidate.status] === statusRank[best.status] && candidate.score > best.score)
922
+ best = candidate;
923
+ }
924
+ return best;
925
+ }
926
+
927
+ export { sniffImage, localizeAssets, countWords, canonicalizeUrl, extractionShowsAccessControl, scoreExtraction, extractPage, chooseBestExtraction };