@nitpicker/crawler 0.13.0 → 0.15.0

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 (171) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +108 -9
  4. package/lib/archive/archive.js +109 -7
  5. package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
  6. package/lib/archive/body-hash/compute-body-hash.js +31 -0
  7. package/lib/archive/body-hash/extract-body.d.ts +18 -0
  8. package/lib/archive/body-hash/extract-body.js +29 -0
  9. package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
  10. package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
  11. package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
  12. package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
  13. package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
  14. package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
  15. package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
  16. package/lib/archive/cache/clear-archive-cache-root.js +32 -0
  17. package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
  18. package/lib/archive/cache/list-archive-cache-entries.js +125 -0
  19. package/lib/archive/cache/path-exists.d.ts +9 -0
  20. package/lib/archive/cache/path-exists.js +18 -0
  21. package/lib/archive/cache/types.d.ts +33 -0
  22. package/lib/archive/cache/types.js +1 -0
  23. package/lib/archive/create-adjunct-tables.d.ts +18 -0
  24. package/lib/archive/create-adjunct-tables.js +277 -0
  25. package/lib/archive/create-entity-tables.d.ts +14 -0
  26. package/lib/archive/create-entity-tables.js +52 -1
  27. package/lib/archive/database.d.ts +91 -2
  28. package/lib/archive/database.js +128 -0
  29. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
  30. package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
  31. package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
  32. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
  33. package/lib/archive/db-ops/_shared/types.d.ts +2 -0
  34. package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
  35. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +29 -0
  36. package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
  37. package/lib/archive/db-ops/analysis/types.d.ts +61 -0
  38. package/lib/archive/db-ops/analysis/types.js +1 -0
  39. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  40. package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
  41. package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
  42. package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
  43. package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
  44. package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
  45. package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
  46. package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
  47. package/lib/archive/db-ops/console-logs/types.js +1 -0
  48. package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
  49. package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
  50. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
  51. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
  52. package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
  53. package/lib/archive/db-ops/lifecycle/init.d.ts +25 -10
  54. package/lib/archive/db-ops/lifecycle/init.js +39 -10
  55. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  56. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  57. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  58. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  59. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  60. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  61. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  62. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  63. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  64. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  65. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  66. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  67. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  68. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  69. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  70. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  71. package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
  72. package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
  73. package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
  74. package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
  75. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
  76. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
  77. package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
  78. package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
  79. package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
  80. package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
  81. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  82. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  83. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
  84. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
  85. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  86. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  87. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  88. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  89. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  90. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  91. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  92. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  93. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  94. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  95. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  96. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  97. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  98. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  99. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  100. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  101. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  102. package/lib/archive/db-ops/pages/write/update-page.js +33 -0
  103. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
  104. package/lib/archive/filesystem/output-binary.d.ts +12 -0
  105. package/lib/archive/filesystem/output-binary.js +17 -0
  106. package/lib/archive/get-failed-page-messages.d.ts +20 -8
  107. package/lib/archive/get-failed-page-messages.js +29 -15
  108. package/lib/archive/init-schema.js +2 -1
  109. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  110. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  111. package/lib/archive/meta/types.d.ts +263 -0
  112. package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
  113. package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
  114. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  115. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  116. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
  117. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
  118. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  119. package/lib/archive/migrate-main-contents-columns.js +50 -0
  120. package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
  121. package/lib/archive/migrate-page-meta-body-hash.js +52 -0
  122. package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
  123. package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
  124. package/lib/archive/page.d.ts +145 -3
  125. package/lib/archive/page.js +195 -0
  126. package/lib/archive/types.d.ts +86 -2
  127. package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
  128. package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
  129. package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
  130. package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
  131. package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
  132. package/lib/archive/url-alias/format-alias-key.js +14 -0
  133. package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
  134. package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
  135. package/lib/archive/url-alias/types.d.ts +7 -0
  136. package/lib/archive/url-alias/types.js +1 -0
  137. package/lib/crawler/choose-probe-host.d.ts +24 -0
  138. package/lib/crawler/choose-probe-host.js +38 -0
  139. package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
  140. package/lib/crawler/crawler.js +261 -2
  141. package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
  142. package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
  143. package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
  144. package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
  145. package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
  146. package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
  147. package/lib/crawler/fetch-destination.js +2 -0
  148. package/lib/crawler/link-to-page-data.js +2 -0
  149. package/lib/crawler/network-gate.d.ts +49 -0
  150. package/lib/crawler/network-gate.js +78 -0
  151. package/lib/crawler/network-outage-detector.d.ts +51 -0
  152. package/lib/crawler/network-outage-detector.js +81 -0
  153. package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
  154. package/lib/crawler/network-outage-summary-counter.js +18 -0
  155. package/lib/crawler/probe-network.d.ts +26 -0
  156. package/lib/crawler/probe-network.js +21 -0
  157. package/lib/crawler/resource-to-page-data.js +2 -0
  158. package/lib/crawler/types.d.ts +152 -1
  159. package/lib/crawler-orchestrator.d.ts +62 -18
  160. package/lib/crawler-orchestrator.js +163 -26
  161. package/lib/crawler.d.ts +21 -0
  162. package/lib/crawler.js +17 -0
  163. package/lib/is-within-outage-window.d.ts +49 -0
  164. package/lib/is-within-outage-window.js +33 -0
  165. package/lib/network-related-error-kinds.d.ts +23 -0
  166. package/lib/network-related-error-kinds.js +28 -0
  167. package/lib/types.d.ts +9 -1
  168. package/lib/utils/array/each-splitted.d.ts +1 -1
  169. package/lib/utils/compute-file-sha256.d.ts +17 -17
  170. package/lib/utils/compute-file-sha256.js +18 -47
  171. package/package.json +3 -3
@@ -13,6 +13,7 @@ import { dnsBurnedHostCache } from './crawler/dns-burned-host-cache.js';
13
13
  import { dnsBurnedHostShortCircuitCounter } from './crawler/dns-burned-host-short-circuit-counter.js';
14
14
  import { findScopeEntry } from './crawler/find-scope-entry.js';
15
15
  import { isLikelyHtmlUrl } from './crawler/is-likely-html-url.js';
16
+ import { networkOutageSummaryCounter } from './crawler/network-outage-summary-counter.js';
16
17
  import { PreloadShortCircuitError } from './crawler/preload-short-circuit-error.js';
17
18
  import { protocolAgnosticKey } from './crawler/protocol-agnostic-key.js';
18
19
  import { crawlerLog, log } from './debug.js';
@@ -65,6 +66,17 @@ export class CrawlerOrchestrator extends EventEmitter {
65
66
  #crawler;
66
67
  /** Whether the crawl was started from a pre-defined URL list (non-recursive mode). */
67
68
  #fromList;
69
+ /**
70
+ * The `network_outages` row id for the currently-open outage, or `null`
71
+ * when none is open. Set by the `networkOutageConfirmed` handler (once
72
+ * the INSERT resolves) and consumed by `networkOutageRecovered` — the
73
+ * `Crawler` class never touches the archive itself and has no way to
74
+ * know the row's id, so the orchestrator is the only place that can
75
+ * bridge the two events for the same outage.
76
+ */
77
+ #openNetworkOutageId = null;
78
+ /** `startedAt` of the currently-open outage, tracked alongside {@link #openNetworkOutageId} so `networkOutageRecovered` can compute a duration for {@link networkOutageSummaryCounter}. */
79
+ #openNetworkOutageStartedAt = null;
68
80
  /** Serializes archive writes from crawler event handlers (FIFO). */
69
81
  #writeQueue = new WriteQueue();
70
82
  /**
@@ -109,6 +121,7 @@ export class CrawlerOrchestrator extends EventEmitter {
109
121
  verbose: options?.verbose ?? false,
110
122
  userAgent: options?.userAgent || defaultUserAgent,
111
123
  ignoreRobots: options?.ignoreRobots ?? false,
124
+ mainContentSelector: options?.mainContentSelector ?? null,
112
125
  // Let the crawler reuse sub-resource data captured during page
113
126
  // rendering instead of issuing a redundant HEAD pre-flight.
114
127
  lookupResource: async (urls) => {
@@ -137,6 +150,14 @@ export class CrawlerOrchestrator extends EventEmitter {
137
150
  // rows continue to land in pages/resources with the DB DEFAULT
138
151
  // `'crawled'` provenance label.
139
152
  inventoryMode: options?.inventoryMode ?? null,
153
+ // Forwarded as-is (including `undefined`) — `Crawler`'s own
154
+ // constructor merges each against its `DEFAULT_NETWORK_OUTAGE_*`
155
+ // constant, so omitting them here is exactly "use the default".
156
+ networkOutageWindowMs: options?.networkOutageWindowMs,
157
+ networkOutageErrorThreshold: options?.networkOutageErrorThreshold,
158
+ networkOutageHostThreshold: options?.networkOutageHostThreshold,
159
+ networkOutageProbeIntervalMs: options?.networkOutageProbeIntervalMs,
160
+ networkProbe: options?.networkProbe ?? null,
140
161
  });
141
162
  }
142
163
  /**
@@ -165,6 +186,11 @@ export class CrawlerOrchestrator extends EventEmitter {
165
186
  */
166
187
  async crawling(list, opts) {
167
188
  const writeQueue = this.#writeQueue;
189
+ // Per-session state, like `Crawler`'s own `#successfulHosts.clear()` /
190
+ // `#networkGate.open()` reset at the start of `#runDeal` — a fresh
191
+ // session must not inherit a dangling outage id from a prior one.
192
+ this.#openNetworkOutageId = null;
193
+ this.#openNetworkOutageStartedAt = null;
168
194
  return new Promise((resolve, reject) => {
169
195
  this.#crawler.on('error', (error) => {
170
196
  if (error.error instanceof PreloadShortCircuitError) {
@@ -211,6 +237,67 @@ export class CrawlerOrchestrator extends EventEmitter {
211
237
  .catch((error) => reject(error));
212
238
  void this.emit('redirect', { result });
213
239
  });
240
+ this.#crawler.on('networkOutageConfirmed', ({ startedAt, detectedAt, probeHost, triggerErrorCount, triggerHostCount }) => {
241
+ crawlerLog('Network outage confirmed: probeHost=%s triggerErrorCount=%d triggerHostCount=%d', probeHost, triggerErrorCount, triggerHostCount);
242
+ // event notice; mirrors `#finalizeCrawlSession`'s unconditional
243
+ // `console.error` for the DNS-burn short-circuit summary.
244
+ console.error(`[network] outage suspected — pausing workers (probe host: ${probeHost ?? 'none'})`);
245
+ writeQueue
246
+ .enqueue(async () => {
247
+ // Both fields are set together, inside this single
248
+ // closure, so the pair can never fall out of sync
249
+ // (e.g. one set synchronously above while the other
250
+ // waits on the INSERT) — `networkOutageRecovered`'s
251
+ // queued closure always sees either both set or
252
+ // neither.
253
+ const id = await this.#archive.insertNetworkOutage({
254
+ startedAt,
255
+ detectedAt,
256
+ probeHost,
257
+ triggerErrorCount,
258
+ triggerHostCount,
259
+ });
260
+ this.#openNetworkOutageId = id;
261
+ this.#openNetworkOutageStartedAt = startedAt;
262
+ })
263
+ .catch((error) => reject(error));
264
+ });
265
+ this.#crawler.on('networkOutageRecovered', ({ endedAt }) => {
266
+ // The `id` read is deferred to INSIDE the queued closure, not
267
+ // read synchronously here, because `networkOutageConfirmed`'s
268
+ // INSERT is itself only queued (not awaited) when that event
269
+ // fires — `#openNetworkOutageId` is not guaranteed to be set
270
+ // yet at the instant `networkOutageRecovered` fires (the two
271
+ // events can arrive in quick succession, e.g. in tests that
272
+ // drive them back-to-back with no real probe-interval delay
273
+ // between them). `WriteQueue` runs enqueued operations in
274
+ // submission order, so by the time THIS closure actually
275
+ // executes, the confirm's INSERT closure (enqueued first) has
276
+ // already completed and `#openNetworkOutageId` is reliably set.
277
+ writeQueue
278
+ .enqueue(() => {
279
+ const id = this.#openNetworkOutageId;
280
+ const startedAt = this.#openNetworkOutageStartedAt;
281
+ if (id === null) {
282
+ // Defensive: `networkOutageConfirmed` always
283
+ // precedes `networkOutageRecovered` on the same
284
+ // `Crawler` instance. If this fires anyway, there
285
+ // is no row to close.
286
+ crawlerLog('Network outage recovered but no open outage id was tracked');
287
+ return Promise.resolve();
288
+ }
289
+ this.#openNetworkOutageId = null;
290
+ this.#openNetworkOutageStartedAt = null;
291
+ const durationMs = endedAt - (startedAt ?? endedAt);
292
+ networkOutageSummaryCounter.confirmedCount++;
293
+ networkOutageSummaryCounter.totalDurationMs += durationMs;
294
+ crawlerLog('Network outage recovered: id=%d endedAt=%d', id, endedAt);
295
+ // eslint-disable-next-line no-console -- see the confirmed handler above
296
+ console.error(`[network] recovered after ${Math.round(durationMs / 1000)}s`);
297
+ return this.#archive.closeNetworkOutage(id, endedAt);
298
+ })
299
+ .catch((error) => reject(error));
300
+ });
214
301
  this.#crawler.on('response', ({ resource, source }) => {
215
302
  writeQueue
216
303
  .enqueue(() => this.#archive.setResources(resource, source))
@@ -221,6 +308,11 @@ export class CrawlerOrchestrator extends EventEmitter {
221
308
  .enqueue(() => this.#archive.setResourcesReferrers(resource))
222
309
  .catch((error) => reject(error));
223
310
  });
311
+ this.#crawler.on('consoleLogs', ({ pageUrl, redirectPaths, entries }) => {
312
+ writeQueue
313
+ .enqueue(() => this.#archive.setConsoleLogs(pageUrl, redirectPaths, entries))
314
+ .catch((error) => reject(error));
315
+ });
224
316
  this.#crawler.on('crawlEnd', () => {
225
317
  writeQueue
226
318
  .drain()
@@ -337,6 +429,7 @@ export class CrawlerOrchestrator extends EventEmitter {
337
429
  disableQueries,
338
430
  userAgent: options?.userAgent || defaultUserAgent,
339
431
  ignoreRobots: options?.ignoreRobots ?? false,
432
+ mainContentSelector: options?.mainContentSelector ?? null,
340
433
  });
341
434
  const orchestrator = new CrawlerOrchestrator(archive, {
342
435
  ...options,
@@ -385,7 +478,13 @@ export class CrawlerOrchestrator extends EventEmitter {
385
478
  const absFilePath = path.isAbsolute(archivePath)
386
479
  ? archivePath
387
480
  : path.resolve(cwd, archivePath);
388
- const archive = await Archive.open({ filePath: absFilePath, cwd });
481
+ // See `ArchiveOpenOptions.openPluginData` for why this must be `true`
482
+ // on every writer path that calls `write()`.
483
+ const archive = await Archive.open({
484
+ filePath: absFilePath,
485
+ cwd,
486
+ openPluginData: true,
487
+ });
389
488
  // Any throw between here and the successful return must release the
390
489
  // archive lock and clean up tmpDir; the caller's `close()` only runs on
391
490
  // the happy path. Errors from `close()` itself are intentionally
@@ -479,22 +578,29 @@ export class CrawlerOrchestrator extends EventEmitter {
479
578
  * 3. Reject archives with unfinished `pending` URLs — those would inherit
480
579
  * the inventory `source` label by mistake. Operator must resume /
481
580
  * retry-failed first.
482
- * 4. Parse the URL list. Anything outside the archived scope is warned
483
- * and skipped (inventory is per-server by design).
484
- * 5. Subtract URLs that already exist in `pages` or `resources` so the
581
+ * 4. If `source` is given, archive its exact bytes under
582
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
583
+ * Done before scope classification so even a run that discards every
584
+ * URL (out of scope or already known) still leaves a recoverable
585
+ * copy of what was fed in.
586
+ * 5. Parse the URL list — the CLI has already warned-and-dropped
587
+ * unparseable-URL lines before calling this method, so every
588
+ * remaining entry parses. Anything outside the archived scope is
589
+ * warned and skipped (inventory is per-server by design).
590
+ * 6. Subtract URLs that already exist in `pages` or `resources` so the
485
591
  * second (and N-th) inventory pass is a no-op for known rows — keeps
486
592
  * `'inventory-seed'` rows from being silently demoted.
487
- * 6. Make `<archive>.bak`. Anything thrown beyond this point restores
593
+ * 7. Make `<archive>.bak`. Anything thrown beyond this point restores
488
594
  * from the backup.
489
- * 7. HEAD-probe each novel URL. Responses classified as HTML are queued
595
+ * 8. HEAD-probe each novel URL. Responses classified as HTML are queued
490
596
  * as Crawler seeds (`'inventory-seed'`); everything else is recorded
491
597
  * in `resources` directly as `'inventory-seed'` (no browser launch).
492
- * 8. If any HTML seeds exist, start a Crawler with
598
+ * 9. If any HTML seeds exist, start a Crawler with
493
599
  * `inventoryMode = { seedUrls }` so the rendered page and every newly
494
600
  * discovered downstream link is labelled correctly. `resume` is fed
495
601
  * the existing `scraped` / `resources` sets so links into already-
496
602
  * crawled pages stop at the seen-gate without re-rendering.
497
- * 9. Drop the backup on success; restore it on any throw.
603
+ * 10. Drop the backup on success; restore it on any throw.
498
604
  *
499
605
  * Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
500
606
  * `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
@@ -504,20 +610,22 @@ export class CrawlerOrchestrator extends EventEmitter {
504
610
  * @param inventoryUrls - Pre-read URL list (one URL per element).
505
611
  * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
506
612
  * @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
507
- * @param sourceFileSha256 - **Pre-computed** SHA-256 hex digest of the
508
- * source URL list. The orchestrator deliberately does NOT receive
509
- * the file path: the path is privacy-sensitive (leaks user-home /
510
- * OS structure when archives are shared) and we want it lifted off
511
- * this boundary so no future log line / breadcrumb / error message
512
- * inside the orchestrator can accidentally re-leak it. The CLI
513
- * computes the digest via `computeFileSha256(resolvedListFile)`
514
- * and passes it through here. Pass `null` for programmatic
515
- * callers that built `inventoryUrls` in-memory; the audit row's
516
- * `source_file_sha256` column will be `NULL`.
613
+ * @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
614
+ * The orchestrator deliberately does NOT receive the file path: the
615
+ * path is privacy-sensitive (leaks user-home / OS structure when
616
+ * archives are shared) and we want it lifted off this boundary so no
617
+ * future log line / breadcrumb / error message inside the orchestrator
618
+ * can accidentally re-leak it. `bytes` is archived verbatim under
619
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
620
+ * before scope classification, so a later `--inventory` run against
621
+ * the same list is an audit no-op even when it discards zero new
622
+ * URLs. Pass `null` for programmatic callers that built
623
+ * `inventoryUrls` in-memory; the audit row's `source_file_sha256`
624
+ * column will be `NULL` and no source list is archived.
517
625
  * @returns The orchestrator instance after a successful inventory pass.
518
626
  * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
519
627
  */
520
- static async inventory(archivePath, inventoryUrls, options, initializedCallback, sourceFileSha256 = null) {
628
+ static async inventory(archivePath, inventoryUrls, options, initializedCallback, source = null) {
521
629
  if (inventoryUrls.length === 0) {
522
630
  throw new Error('inventory: URL list is empty');
523
631
  }
@@ -525,7 +633,13 @@ export class CrawlerOrchestrator extends EventEmitter {
525
633
  const absFilePath = path.isAbsolute(archivePath)
526
634
  ? archivePath
527
635
  : path.resolve(cwd, archivePath);
528
- const archive = await Archive.open({ filePath: absFilePath, cwd });
636
+ // See `ArchiveOpenOptions.openPluginData` for why this must be `true`
637
+ // on every writer path that calls `write()`.
638
+ const archive = await Archive.open({
639
+ filePath: absFilePath,
640
+ cwd,
641
+ openPluginData: true,
642
+ });
529
643
  try {
530
644
  const archived = await archive.getConfig();
531
645
  if (archived.fromList) {
@@ -547,6 +661,14 @@ export class CrawlerOrchestrator extends EventEmitter {
547
661
  // eslint-disable-next-line no-console -- operator-facing warning, must be visible regardless of DEBUG filters
548
662
  console.warn(`inventory: archive has ${pending.length} pending URLs from a previous crawl. Proceeding — crawled-wins priority keeps their labels stable. Consider \`--resume\` first if you want the prior work finalized.`);
549
663
  }
664
+ // Archive the exact source bytes before scope classification, so
665
+ // even a run that discards every URL (out-of-scope or already
666
+ // known — see `novelUrls.length === 0` below) still leaves a
667
+ // recoverable copy of what was fed in. Content-hash-named, so a
668
+ // second `--inventory` pass with the same list is a no-op write.
669
+ if (source) {
670
+ await archive.saveInventorySourceList(source.sha256, source.bytes);
671
+ }
550
672
  // Parse + scope-classify the candidate URLs. sortUrl drops
551
673
  // unparseable strings; findScopeEntry separates in-scope from
552
674
  // out-of-scope.
@@ -701,7 +823,8 @@ export class CrawlerOrchestrator extends EventEmitter {
701
823
  htmlSeedsCount: htmlSeeds.length,
702
824
  nonHtmlCount: nonHtmlSeeds.length,
703
825
  outOfScope,
704
- sourceFileSha256,
826
+ sourceFileSha256: source?.sha256 ?? null,
827
+ invalidSkipped: source?.invalidLineCount ?? null,
705
828
  });
706
829
  // Ingestion's DB writes are now committed. From here on a
707
830
  // throw must NOT trigger the `.bak` restore (it would wipe
@@ -851,7 +974,13 @@ export class CrawlerOrchestrator extends EventEmitter {
851
974
  const absFilePath = path.isAbsolute(archivePath)
852
975
  ? archivePath
853
976
  : path.resolve(cwd, archivePath);
854
- const archive = await Archive.open({ filePath: absFilePath, cwd });
977
+ // See `ArchiveOpenOptions.openPluginData` for why this must be `true`
978
+ // on every writer path that calls `write()`.
979
+ const archive = await Archive.open({
980
+ filePath: absFilePath,
981
+ cwd,
982
+ openPluginData: true,
983
+ });
855
984
  // Any throw between here and the successful return must release the
856
985
  // archive lock and clean up tmpDir; the caller's `close()` only runs on
857
986
  // the happy path.
@@ -988,10 +1117,10 @@ export class CrawlerOrchestrator extends EventEmitter {
988
1117
  * no `--label` flag, so this is always the auto form.
989
1118
  * `source_file_sha256` arrives pre-computed via
990
1119
  * `aggregates.sourceFileSha256` (the CLI's `inventoryCrawl` ran
991
- * `computeFileSha256` against the input txt before the orchestrator
992
- * was even invoked). The orchestrator boundary deliberately never sees
993
- * the absolute path — see {@link InventoryRunAggregates} for the
994
- * privacy rationale.
1120
+ * `computeFileSha256` against the bytes it read from the input txt,
1121
+ * before the orchestrator was even invoked). The orchestrator boundary
1122
+ * deliberately never sees the absolute path — see
1123
+ * {@link InventoryRunAggregates} for the privacy rationale.
995
1124
  *
996
1125
  * **Audit-write failures abort the ingestion phase.** Swallowing them
997
1126
  * would only be justified if the audit were the last write after the
@@ -1018,6 +1147,7 @@ export class CrawlerOrchestrator extends EventEmitter {
1018
1147
  new_pages: aggregates.htmlSeedsCount,
1019
1148
  new_resources: aggregates.nonHtmlCount,
1020
1149
  scope_skipped: aggregates.outOfScope,
1150
+ invalid_skipped: aggregates.invalidSkipped,
1021
1151
  });
1022
1152
  }
1023
1153
  /**
@@ -1032,6 +1162,13 @@ export class CrawlerOrchestrator extends EventEmitter {
1032
1162
  // eslint-disable-next-line no-console
1033
1163
  console.error(`[preload] Short-circuited ${skipped} URL(s) on DNS-burned hosts`);
1034
1164
  }
1165
+ const { confirmedCount, totalDurationMs } = networkOutageSummaryCounter;
1166
+ if (confirmedCount > 0) {
1167
+ // eslint-disable-next-line no-console
1168
+ console.error(`[network] ${confirmedCount} outage(s), ${Math.round(totalDurationMs / 1000)}s total`);
1169
+ }
1170
+ networkOutageSummaryCounter.confirmedCount = 0;
1171
+ networkOutageSummaryCounter.totalDurationMs = 0;
1035
1172
  clearDestinationCache();
1036
1173
  clearDnsBurnedHostCache();
1037
1174
  }
package/lib/crawler.d.ts CHANGED
@@ -16,16 +16,36 @@ export { default as ArchiveResource } from './archive/resource.js';
16
16
  export * from './archive/types.js';
17
17
  export { default as Archive } from './archive/archive.js';
18
18
  export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
19
+ export { getArchiveCacheRoot } from './archive/cache/get-archive-cache-root.js';
20
+ export { computeArchiveCacheKey } from './archive/cache/compute-archive-cache-key.js';
21
+ export { resolveArchiveCacheDir } from './archive/cache/resolve-archive-cache-dir.js';
22
+ export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entries.js';
23
+ export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
24
+ export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
25
+ export type { ArchiveCacheEntry, ArchiveCacheEntryKind } from './archive/cache/types.js';
19
26
  export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
20
27
  export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
21
28
  export type { ArchiveLockHolder } from './archive/peek-archive-lock.js';
22
29
  export type { FlatPageMetaColumns, JsonLdRow, JsonLdRowForInsert, TagRow, TagRowForInsert, JsonLdSummary, TagsSummary, TagInventoryEntry, PageDenormalizedColumns, } from './archive/meta/types.js';
23
30
  export { IncompatibleArchiveError } from './archive/meta/types.js';
24
31
  export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
32
+ export { computeBodyHash } from './archive/body-hash/compute-body-hash.js';
33
+ export { decodeStoredBlob } from './archive/decode-html-blob.js';
34
+ export { computeTierAAliasKey } from './archive/url-alias/compute-tier-a-alias-key.js';
35
+ export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-key.js';
25
36
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
26
37
  export * from './types.js';
27
38
  export * from './crawler/types.js';
28
39
  export { classifyErrorKind } from './classify-error-kind.js';
40
+ export { NETWORK_RELATED_ERROR_KINDS } from './network-related-error-kinds.js';
41
+ export type { OutageWindow } from './is-within-outage-window.js';
42
+ export { isWithinOutageWindow } from './is-within-outage-window.js';
43
+ export { default as NetworkOutageDetector } from './crawler/network-outage-detector.js';
44
+ export { default as NetworkGate } from './crawler/network-gate.js';
45
+ export type { NetworkProbe } from './crawler/probe-network.js';
46
+ export { probeNetwork } from './crawler/probe-network.js';
47
+ export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
48
+ export { chooseProbeHost } from './crawler/choose-probe-host.js';
29
49
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
30
50
  export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
31
51
  export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
@@ -38,3 +58,4 @@ export { populateBlobRefs } from './archive/populate-ref-tables/populate-blob-re
38
58
  export { populateHeaderTables } from './archive/populate-ref-tables/populate-header-tables.js';
39
59
  export { loadResponseHeadersBySetIds } from './archive/db-ops/_shared/load-response-headers-by-set-ids.js';
40
60
  export { decodeJsonRef } from './archive/db-ops/_shared/decode-json-ref.js';
61
+ export type { TemplateClusterBlockingEvidence, TemplateClusterBlockingReason, TemplateClusterLandmarkProfile, TemplateClusterLandmarkType, TemplateClusterReason, } from './archive/db-ops/analysis/types.js';
package/lib/crawler.js CHANGED
@@ -17,15 +17,32 @@ export { default as ArchiveResource } from './archive/resource.js';
17
17
  export * from './archive/types.js';
18
18
  export { default as Archive } from './archive/archive.js';
19
19
  export { isArchiveCacheDisabled } from './archive/cache/is-archive-cache-disabled.js';
20
+ export { getArchiveCacheRoot } from './archive/cache/get-archive-cache-root.js';
21
+ export { computeArchiveCacheKey } from './archive/cache/compute-archive-cache-key.js';
22
+ export { resolveArchiveCacheDir } from './archive/cache/resolve-archive-cache-dir.js';
23
+ export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entries.js';
24
+ export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
25
+ export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
20
26
  export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
21
27
  export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
22
28
  export { IncompatibleArchiveError } from './archive/meta/types.js';
23
29
  export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
30
+ export { computeBodyHash } from './archive/body-hash/compute-body-hash.js';
31
+ export { decodeStoredBlob } from './archive/decode-html-blob.js';
32
+ export { computeTierAAliasKey } from './archive/url-alias/compute-tier-a-alias-key.js';
33
+ export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-key.js';
24
34
  // Core
25
35
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
26
36
  export * from './types.js';
27
37
  export * from './crawler/types.js';
28
38
  export { classifyErrorKind } from './classify-error-kind.js';
39
+ export { NETWORK_RELATED_ERROR_KINDS } from './network-related-error-kinds.js';
40
+ export { isWithinOutageWindow } from './is-within-outage-window.js';
41
+ export { default as NetworkOutageDetector } from './crawler/network-outage-detector.js';
42
+ export { default as NetworkGate } from './crawler/network-gate.js';
43
+ export { probeNetwork } from './crawler/probe-network.js';
44
+ export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
45
+ export { chooseProbeHost } from './crawler/choose-probe-host.js';
29
46
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
30
47
  // 0.13 ref-table population (issue #191, epic #103). Exposed as the
31
48
  // public seam that the migration script (`scripts/migrate-to-0.13.mjs`)
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A resolved (closed) network-outage interval, as consumed by
3
+ * {@link isWithinOutageWindow}.
4
+ *
5
+ * This is deliberately NOT the raw `network_outages` row shape: a row whose
6
+ * `ended_at` is still `NULL` (the crawl session crashed before recovery was
7
+ * observed) must be resolved to a concrete `endedAt` by the caller — e.g.
8
+ * `listNetworkOutages` clamping it to the latest observed timestamp in the
9
+ * archive — before it reaches this function. Accepting a nullable `endedAt`
10
+ * here would let a crashed session's outage silently swallow every
11
+ * subsequent timestamp as "network-caused" forever.
12
+ */
13
+ export interface OutageWindow {
14
+ /** Epoch ms the outage is considered to have started (already backdated to the earliest triggering error). */
15
+ readonly startedAt: number;
16
+ /** Epoch ms the outage is considered to have ended. Never `NULL` — see the interface docstring. */
17
+ readonly endedAt: number;
18
+ }
19
+ /**
20
+ * Decide whether a timestamp falls inside any recorded network-outage
21
+ * window — i.e. whether an error observed at that instant is more likely
22
+ * attributable to the operator's own network than to the target site.
23
+ *
24
+ * Pure and dependency-free by design: no DB handle, no `Crawler` instance.
25
+ * This is the single predicate every consumer (`resetFailedPages`,
26
+ * `listDnsBurnedHostCandidates`, `getSummary`'s attribution split) is
27
+ * expected to call, so the inclusive/exclusive boundary decision below is
28
+ * made exactly once.
29
+ *
30
+ * **Boundaries are inclusive on both ends.** A timestamp equal to
31
+ * `startedAt` or `endedAt` counts as inside the window. This errs toward
32
+ * attributing borderline errors to the outage rather than to the site:
33
+ * classifying a genuinely network-caused failure as "unknown/site kills it
34
+ * forever" (a false negative) is worse than the reverse (a false positive
35
+ * merely costs one extra retry pass before the kind classifier sorts it out
36
+ * again).
37
+ * @param timestamp - Epoch ms to test (typically an error's `createdAt`).
38
+ * @param windows - Resolved outage windows to test against. An empty array
39
+ * always yields `false` — this is what makes an archive with no
40
+ * `network_outages` rows (every archive created before this feature, or
41
+ * any crawl with no detected outage) behave identically to today.
42
+ * @returns `true` if `timestamp` falls within any window.
43
+ * @example
44
+ * ```ts
45
+ * isWithinOutageWindow(1_000, [{ startedAt: 500, endedAt: 1_500 }]); // true
46
+ * isWithinOutageWindow(1_000, []); // false — no recorded outages
47
+ * ```
48
+ */
49
+ export declare function isWithinOutageWindow(timestamp: number, windows: readonly OutageWindow[]): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Decide whether a timestamp falls inside any recorded network-outage
3
+ * window — i.e. whether an error observed at that instant is more likely
4
+ * attributable to the operator's own network than to the target site.
5
+ *
6
+ * Pure and dependency-free by design: no DB handle, no `Crawler` instance.
7
+ * This is the single predicate every consumer (`resetFailedPages`,
8
+ * `listDnsBurnedHostCandidates`, `getSummary`'s attribution split) is
9
+ * expected to call, so the inclusive/exclusive boundary decision below is
10
+ * made exactly once.
11
+ *
12
+ * **Boundaries are inclusive on both ends.** A timestamp equal to
13
+ * `startedAt` or `endedAt` counts as inside the window. This errs toward
14
+ * attributing borderline errors to the outage rather than to the site:
15
+ * classifying a genuinely network-caused failure as "unknown/site kills it
16
+ * forever" (a false negative) is worse than the reverse (a false positive
17
+ * merely costs one extra retry pass before the kind classifier sorts it out
18
+ * again).
19
+ * @param timestamp - Epoch ms to test (typically an error's `createdAt`).
20
+ * @param windows - Resolved outage windows to test against. An empty array
21
+ * always yields `false` — this is what makes an archive with no
22
+ * `network_outages` rows (every archive created before this feature, or
23
+ * any crawl with no detected outage) behave identically to today.
24
+ * @returns `true` if `timestamp` falls within any window.
25
+ * @example
26
+ * ```ts
27
+ * isWithinOutageWindow(1_000, [{ startedAt: 500, endedAt: 1_500 }]); // true
28
+ * isWithinOutageWindow(1_000, []); // false — no recorded outages
29
+ * ```
30
+ */
31
+ export function isWithinOutageWindow(timestamp, windows) {
32
+ return windows.some((window) => timestamp >= window.startedAt && timestamp <= window.endedAt);
33
+ }
@@ -0,0 +1,23 @@
1
+ import type { ErrorKind } from './types.js';
2
+ /**
3
+ * `ErrorKind`s that plausibly indicate trouble with the operator's own
4
+ * network path rather than a genuinely dead or misconfigured target site.
5
+ *
6
+ * Single source of truth shared by two consumers that must agree on
7
+ * exactly the same set:
8
+ *
9
+ * - `NetworkOutageDetector` — only these kinds contribute to the sliding
10
+ * window that detects a suspect outage.
11
+ * - `evict-network-classified-destination-cache-entries.ts` — on recovery,
12
+ * only `destinationCache` entries whose cached `Error` classifies into
13
+ * one of these kinds are evicted (a cached `tls` or `client-blocked`
14
+ * failure is a site-specific fact and must survive an outage recovery
15
+ * unrelated to it).
16
+ *
17
+ * `dns` is included despite `permanent-error-kinds.ts` treating it as a
18
+ * permanent, site-specific verdict in isolation — that classification is
19
+ * exactly what outage detection exists to override when the surrounding
20
+ * evidence (many hosts, tight time window) points to the local network
21
+ * instead.
22
+ */
23
+ export declare const NETWORK_RELATED_ERROR_KINDS: ReadonlySet<ErrorKind>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `ErrorKind`s that plausibly indicate trouble with the operator's own
3
+ * network path rather than a genuinely dead or misconfigured target site.
4
+ *
5
+ * Single source of truth shared by two consumers that must agree on
6
+ * exactly the same set:
7
+ *
8
+ * - `NetworkOutageDetector` — only these kinds contribute to the sliding
9
+ * window that detects a suspect outage.
10
+ * - `evict-network-classified-destination-cache-entries.ts` — on recovery,
11
+ * only `destinationCache` entries whose cached `Error` classifies into
12
+ * one of these kinds are evicted (a cached `tls` or `client-blocked`
13
+ * failure is a site-specific fact and must survive an outage recovery
14
+ * unrelated to it).
15
+ *
16
+ * `dns` is included despite `permanent-error-kinds.ts` treating it as a
17
+ * permanent, site-specific verdict in isolation — that classification is
18
+ * exactly what outage detection exists to override when the surrounding
19
+ * evidence (many hosts, tight time window) points to the local network
20
+ * instead.
21
+ */
22
+ export const NETWORK_RELATED_ERROR_KINDS = new Set([
23
+ 'dns',
24
+ 'dns-transient',
25
+ 'local-network',
26
+ 'connection-timeout',
27
+ 'connection-reset',
28
+ ]);
package/lib/types.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { CrawlerError, PageData } from './utils/types/types.js';
10
10
  * documented per-property rather than scattered across the two emit sites.
11
11
  */
12
12
  export interface InventoryRunAggregates {
13
- /** Total non-empty lines in the input list (= `inventoryUrls.length` before any filtering). Stored verbatim as `inventory_runs.total_lines`. */
13
+ /** `inventoryUrls.length` as received by `CrawlerOrchestrator.inventory` — the CLI (`inventoryCrawl`) has already warned-and-dropped unparseable-URL lines before this point, so this counts valid URLs, not raw source-file lines. Stored verbatim as `inventory_runs.total_lines`. */
14
14
  inventoryUrlsCount: number;
15
15
  /** Number of novel URLs classified as HTML and queued for render. Stored as `new_pages` (excludes anchor-discovered descendants — those add later via the crawler graph and are NOT counted here). */
16
16
  htmlSeedsCount: number;
@@ -32,6 +32,14 @@ export interface InventoryRunAggregates {
32
32
  * `source_file_sha256` will be `NULL`.
33
33
  */
34
34
  sourceFileSha256: string | null;
35
+ /**
36
+ * Number of source-file lines the CLI warned-and-dropped for failing
37
+ * URL validation, before `inventoryUrlsCount` was ever counted. Stored
38
+ * verbatim as `inventory_runs.invalid_skipped`. `null` for programmatic
39
+ * callers that built `inventoryUrls` in-memory — there is no source
40
+ * file, so no line was ever dropped as invalid.
41
+ */
42
+ invalidSkipped: number | null;
35
43
  }
36
44
  /**
37
45
  * Coarse cause of a crawl/scrape failure.
@@ -7,4 +7,4 @@
7
7
  * @param callback - A function to invoke on each chunk. May be synchronous or asynchronous.
8
8
  * @returns A promise that resolves when all chunk callbacks have completed.
9
9
  */
10
- export declare function eachSplitted<T>(a: T[], count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
10
+ export declare function eachSplitted<T>(a: readonly T[], count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
@@ -1,24 +1,24 @@
1
1
  /**
2
- * Compute the SHA-256 digest of a file's bytes, streaming so memory stays
3
- * O(1) regardless of file size.
2
+ * Compute the SHA-256 digest of an in-memory byte buffer.
4
3
  *
5
- * Used by `CrawlerOrchestrator.inventory` to fingerprint the source
6
- * URL list `.txt` and store the digest on the `inventory_runs` row. The
7
- * digest is the content-identity key for "was this exact list applied
8
- * before" recorded for operator audit; nothing dedupes on it at write
9
- * time.
4
+ * Used by `CrawlerOrchestrator.inventory` to fingerprint the source URL
5
+ * list the digest is both recorded on the `inventory_runs` audit row
6
+ * (the content-identity key for "was this exact list applied before") and
7
+ * used as the file name under which the raw list is archived
8
+ * (`Archive.saveInventorySourceList`).
10
9
  *
11
- * Returns `null` instead of throwing when the file cannot be read (e.g.
12
- * vanished mid-run, permissions issue) so the inventory run's audit row
13
- * can still be written with a NULL digest. Hashing failure is an audit
14
- * loss, not a correctness failure the actual ingestion has already
15
- * succeeded by the time this is called.
16
- * @param filePath - Absolute or relative path to the file to hash.
17
- * @returns Lower-case hex digest (64 chars), or `null` if reading failed.
10
+ * Takes an already-read `Buffer` rather than a file path: the CLI reads the
11
+ * source file exactly once (`inventoryCrawl`) and derives the digest, the
12
+ * parsed URL list, and the archived copy all from that single buffer. A
13
+ * separate read-then-hash pass would let the file change between the two
14
+ * reads and desync the archived bytes from the hash naming them.
15
+ * @param bytes - The exact bytes to hash.
16
+ * @returns Lower-case hex digest (64 chars).
18
17
  * @example
19
18
  * ```ts
20
- * const sha = await computeFileSha256('/tmp/list.txt');
21
- * if (sha) console.log(sha); // 64-char hex string
19
+ * const bytes = await fs.readFile('/tmp/list.txt');
20
+ * const sha = computeFileSha256(bytes);
21
+ * console.log(sha); // 64-char hex string
22
22
  * ```
23
23
  */
24
- export declare function computeFileSha256(filePath: string): Promise<string | null>;
24
+ export declare function computeFileSha256(bytes: Buffer): string;