@nitpicker/crawler 0.14.0 → 0.16.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 (155) hide show
  1. package/lib/archive/archive.d.ts +136 -10
  2. package/lib/archive/archive.js +147 -9
  3. package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
  4. package/lib/archive/body-hash/compute-body-hash.js +31 -0
  5. package/lib/archive/body-hash/extract-body.d.ts +18 -0
  6. package/lib/archive/body-hash/extract-body.js +29 -0
  7. package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
  8. package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
  9. package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
  10. package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
  11. package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
  12. package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
  13. package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
  14. package/lib/archive/cache/clear-archive-cache-root.js +32 -0
  15. package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
  16. package/lib/archive/cache/list-archive-cache-entries.js +125 -0
  17. package/lib/archive/cache/path-exists.d.ts +9 -0
  18. package/lib/archive/cache/path-exists.js +18 -0
  19. package/lib/archive/cache/types.d.ts +33 -0
  20. package/lib/archive/cache/types.js +1 -0
  21. package/lib/archive/create-adjunct-tables.d.ts +14 -0
  22. package/lib/archive/create-adjunct-tables.js +169 -0
  23. package/lib/archive/create-entity-tables.js +21 -1
  24. package/lib/archive/database.d.ts +67 -2
  25. package/lib/archive/database.js +92 -3
  26. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
  27. package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
  28. package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
  29. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
  30. package/lib/archive/db-ops/_shared/types.d.ts +2 -0
  31. package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
  32. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
  33. package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
  34. package/lib/archive/db-ops/analysis/types.d.ts +61 -0
  35. package/lib/archive/db-ops/analysis/types.js +1 -0
  36. package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
  37. package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
  38. package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
  39. package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
  40. package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
  41. package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
  42. package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
  43. package/lib/archive/db-ops/console-logs/types.js +1 -0
  44. package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
  45. package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
  46. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
  47. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
  48. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
  49. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
  50. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
  51. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
  52. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
  53. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
  54. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
  55. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
  56. package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
  57. package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
  58. package/lib/archive/db-ops/lifecycle/init.js +21 -2
  59. package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
  60. package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
  61. package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
  62. package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
  63. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
  64. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
  65. package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
  66. package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
  67. package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
  68. package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
  69. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
  70. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
  71. package/lib/archive/db-ops/pages/write/update-page.js +12 -0
  72. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
  73. package/lib/archive/filesystem/output-binary.d.ts +12 -0
  74. package/lib/archive/filesystem/output-binary.js +17 -0
  75. package/lib/archive/get-failed-page-messages.d.ts +20 -8
  76. package/lib/archive/get-failed-page-messages.js +29 -15
  77. package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
  78. package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
  79. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
  80. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
  81. package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
  82. package/lib/archive/migrate-page-meta-body-hash.js +52 -0
  83. package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
  84. package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
  85. package/lib/archive/page.d.ts +1 -1
  86. package/lib/archive/types.d.ts +60 -2
  87. package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
  88. package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
  89. package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
  90. package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
  91. package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
  92. package/lib/archive/url-alias/format-alias-key.js +14 -0
  93. package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
  94. package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
  95. package/lib/archive/url-alias/types.d.ts +7 -0
  96. package/lib/archive/url-alias/types.js +1 -0
  97. package/lib/classify-error-kind.d.ts +1 -0
  98. package/lib/classify-error-kind.js +14 -0
  99. package/lib/crawler/assert-chrome-installed.d.ts +24 -0
  100. package/lib/crawler/assert-chrome-installed.js +43 -0
  101. package/lib/crawler/choose-probe-host.d.ts +24 -0
  102. package/lib/crawler/choose-probe-host.js +38 -0
  103. package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
  104. package/lib/crawler/crawler.d.ts +12 -0
  105. package/lib/crawler/crawler.js +496 -29
  106. package/lib/crawler/decode-auth-credential.d.ts +29 -0
  107. package/lib/crawler/decode-auth-credential.js +39 -0
  108. package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
  109. package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
  110. package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
  111. package/lib/crawler/dedupe/compute-shape-key.js +56 -0
  112. package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
  113. package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
  114. package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
  115. package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
  116. package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
  117. package/lib/crawler/dedupe/is-shape-capped.js +12 -0
  118. package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
  119. package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
  120. package/lib/crawler/dedupe/types.d.ts +42 -0
  121. package/lib/crawler/dedupe/types.js +1 -0
  122. package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
  123. package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
  124. package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
  125. package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
  126. package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
  127. package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
  128. package/lib/crawler/fetch-destination.js +14 -2
  129. package/lib/crawler/generate-predicted-urls.d.ts +12 -0
  130. package/lib/crawler/generate-predicted-urls.js +33 -2
  131. package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
  132. package/lib/crawler/network-gate.d.ts +49 -0
  133. package/lib/crawler/network-gate.js +78 -0
  134. package/lib/crawler/network-outage-detector.d.ts +51 -0
  135. package/lib/crawler/network-outage-detector.js +81 -0
  136. package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
  137. package/lib/crawler/network-outage-summary-counter.js +18 -0
  138. package/lib/crawler/probe-network.d.ts +26 -0
  139. package/lib/crawler/probe-network.js +21 -0
  140. package/lib/crawler/types.d.ts +185 -1
  141. package/lib/crawler-orchestrator.d.ts +74 -18
  142. package/lib/crawler-orchestrator.js +267 -27
  143. package/lib/crawler.d.ts +22 -0
  144. package/lib/crawler.js +18 -0
  145. package/lib/is-within-outage-window.d.ts +49 -0
  146. package/lib/is-within-outage-window.js +33 -0
  147. package/lib/network-related-error-kinds.d.ts +23 -0
  148. package/lib/network-related-error-kinds.js +28 -0
  149. package/lib/permanent-error-kinds.d.ts +9 -4
  150. package/lib/permanent-error-kinds.js +10 -4
  151. package/lib/types.d.ts +11 -2
  152. package/lib/utils/array/each-splitted.d.ts +1 -1
  153. package/lib/utils/compute-file-sha256.d.ts +17 -17
  154. package/lib/utils/compute-file-sha256.js +18 -47
  155. package/package.json +3 -3
@@ -0,0 +1,21 @@
1
+ import { lookup } from 'node:dns';
2
+ /**
3
+ * Default {@link NetworkProbe}: an active `dns.lookup` against `host`.
4
+ *
5
+ * Deliberately does not hit a hardcoded external address (e.g. `1.1.1.1`) —
6
+ * probing a host that has already answered successfully during this crawl
7
+ * session (see `choose-probe-host.ts`) avoids depending on infrastructure
8
+ * outside the sites actually being crawled. `dns.lookup` alone is enough:
9
+ * it exercises the operator's local resolver / network path, which is
10
+ * exactly the layer an operator-side outage breaks.
11
+ * @param host - Hostname to resolve.
12
+ * @returns `true` if the lookup succeeds, `false` on any error (including
13
+ * NXDOMAIN, timeout, or resolver unavailability).
14
+ */
15
+ export function probeNetwork(host) {
16
+ return new Promise((resolve) => {
17
+ lookup(host, (error) => {
18
+ resolve(!error);
19
+ });
20
+ });
21
+ }
@@ -1,6 +1,8 @@
1
+ import type { NetworkProbe } from './probe-network.js';
1
2
  import type { PageSource } from '../archive/types.js';
3
+ import type { ErrorKind } from '../types.js';
2
4
  import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
3
- import type { ChangePhaseEvent, ScrapeResult } from '@d-zero/beholder';
5
+ import type { ChangePhaseEvent, ConsoleLogEntry, ScrapeResult } from '@d-zero/beholder';
4
6
  import type { ParseURLOptions } from '@d-zero/shared/parse-url';
5
7
  /**
6
8
  * Result of resolving a URL that redirects to a destination already rendered
@@ -160,6 +162,57 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
160
162
  * the DB DEFAULT `'crawled'` applies.
161
163
  */
162
164
  inventoryMode: InventoryMode | null;
165
+ /**
166
+ * Sliding-window size in ms for network-outage suspicion. See
167
+ * `NetworkOutageDetector`.
168
+ */
169
+ networkOutageWindowMs: number;
170
+ /**
171
+ * Minimum error count within {@link networkOutageWindowMs} to declare a
172
+ * suspect outage.
173
+ */
174
+ networkOutageErrorThreshold: number;
175
+ /**
176
+ * Minimum distinct-host count within {@link networkOutageWindowMs} to
177
+ * declare a suspect outage. Guards against a single flaky host looking
178
+ * like a network-wide event.
179
+ */
180
+ networkOutageHostThreshold: number;
181
+ /**
182
+ * Interval in ms between recovery probes while the network gate is
183
+ * closed.
184
+ */
185
+ networkOutageProbeIntervalMs: number;
186
+ /**
187
+ * Injectable network-reachability probe, or `null` to use the default
188
+ * `dns.lookup`-based `probeNetwork`. Overriding this is the seam tests
189
+ * use to simulate confirmed outages and recoveries deterministically
190
+ * without touching the real network.
191
+ */
192
+ networkProbe: NetworkProbe | null;
193
+ /**
194
+ * Same-cluster soft-cap threshold (`--dedupe-cap`), or `null` to disable
195
+ * the feature entirely (the default). When set, {@link Crawler} stops
196
+ * enqueueing newly-discovered URLs whose shape (see `computeShapeKey`)
197
+ * has accumulated this many matching-signature observations (see
198
+ * `DedupeCapTracker`).
199
+ */
200
+ dedupeCap: number | null;
201
+ /**
202
+ * Hard cap on the number of distinct URL shapes the same-cluster soft
203
+ * cap tracks at once (`--dedupe-map-cap`); the least-recently-touched
204
+ * shape is evicted beyond this. Only relevant when {@link dedupeCap} is
205
+ * non-null.
206
+ */
207
+ dedupeMapCap: number;
208
+ /**
209
+ * Shape keys already confirmed capped in a prior session
210
+ * (persisted as `dedupe_cap_events.shape_key`), seeded into the
211
+ * tracker's sticky set so `--resume` / `--append` / `--retry-failed` /
212
+ * `--inventory` do not re-admit a trap this crawl already paid the cost
213
+ * of discovering once. Ignored when {@link dedupeCap} is `null`.
214
+ */
215
+ preloadedStickyShapeKeys: readonly string[];
163
216
  }
164
217
  /**
165
218
  * Inventory-mode runtime configuration. Passed from
@@ -294,6 +347,31 @@ export interface CrawlerEventTypes {
294
347
  /** The URL of the referenced resource (without hash). */
295
348
  src: string;
296
349
  };
350
+ /**
351
+ * Emitted once per scrape with the console messages / page errors
352
+ * beholder captured for that page (issue #228). Only emitted when
353
+ * `entries` is non-empty — see `Crawler#handleConsoleLogs` for why a
354
+ * degraded re-scrape that captures nothing must not clear prior good
355
+ * data.
356
+ */
357
+ consoleLogs: {
358
+ /**
359
+ * The originally-requested URL, normalised (`withoutHashAndAuth`
360
+ * form) — the same identity `updatePage` resolves its redirect
361
+ * chain from, NOT necessarily the page that ends up holding the
362
+ * content.
363
+ */
364
+ pageUrl: string;
365
+ /**
366
+ * The redirect chain hops captured during fetch, in order. Empty
367
+ * when the page was not redirected, or when the scrape produced no
368
+ * `pageData` (a `'skipped'` / `'error'` result) and no redirect
369
+ * information is available.
370
+ */
371
+ redirectPaths: readonly string[];
372
+ /** The captured console messages / page errors, in capture order. */
373
+ entries: ConsoleLogEntry[];
374
+ };
297
375
  /**
298
376
  * Emitted when the entire crawl process has completed or been aborted.
299
377
  */
@@ -349,4 +427,110 @@ export interface CrawlerEventTypes {
349
427
  */
350
428
  source: PageSource | undefined;
351
429
  };
430
+ /**
431
+ * Emitted the instant `Crawler` closes its internal network gate after a
432
+ * recovery probe CONFIRMS a suspect outage (the sliding-window threshold
433
+ * alone only makes it a suspect — see `NetworkOutageDetector`). The
434
+ * orchestrator persists this via `Archive.insertNetworkOutage` and must
435
+ * remember the returned row id to pass to the matching
436
+ * `networkOutageRecovered` event, since `Crawler` itself never touches
437
+ * the archive and has no way to know the row's id.
438
+ */
439
+ networkOutageConfirmed: {
440
+ /** Backdated to the earliest error still inside the detector's window at trigger time. */
441
+ startedAt: number;
442
+ /** When the sliding window actually crossed both thresholds. */
443
+ detectedAt: number;
444
+ /** Hostname the recovery probe is targeting, or `null` if none was available. */
445
+ probeHost: string | null;
446
+ triggerErrorCount: number;
447
+ triggerHostCount: number;
448
+ };
449
+ /**
450
+ * Emitted the instant `Crawler` reopens its internal network gate after
451
+ * a recovery probe succeeds. NOT emitted when the gate is opened
452
+ * because the crawl was aborted while paused — in that case the outage
453
+ * row is deliberately left open for the next writer session's
454
+ * boot-time finalizer to resolve (see
455
+ * `db-ops/outages/close-stale-open-network-outages.ts`), since an abort
456
+ * says nothing about whether the network actually recovered.
457
+ */
458
+ networkOutageRecovered: {
459
+ /** Epoch ms the recovery probe first succeeded. */
460
+ endedAt: number;
461
+ };
462
+ /**
463
+ * Emitted the instant the opt-in same-cluster soft cap
464
+ * ({@link CrawlerOptions.dedupeCap}) confirms a URL shape as a trap (see
465
+ * `DedupeCapTracker`). The orchestrator persists this via
466
+ * `Archive.insertDedupeCapEvent` and must remember the returned row id so
467
+ * `rejected_count` can be finalized once at `crawlEnd` (`Crawler` itself
468
+ * never touches the archive).
469
+ */
470
+ dedupeCap: {
471
+ shapeKey: string;
472
+ sampleUrl: string;
473
+ bodyHash: Buffer;
474
+ effectiveThreshold: number;
475
+ observedCount: number;
476
+ };
477
+ }
478
+ /**
479
+ * Tunables for `NetworkOutageDetector`.
480
+ */
481
+ export interface NetworkOutageDetectorOptions {
482
+ /**
483
+ * Sliding-window size in ms (`W`). Before each check, entries older than
484
+ * `at - windowMs` (inclusive boundary — an entry exactly `windowMs` old
485
+ * still counts) are evicted.
486
+ */
487
+ readonly windowMs: number;
488
+ /** Minimum error count within the window to declare a suspect outage (`N`). */
489
+ readonly errorThreshold: number;
490
+ /**
491
+ * Minimum number of DISTINCT hosts represented in the window to declare
492
+ * a suspect outage (`M`). Guards against one flaky host (a site that is
493
+ * genuinely retrying/failing on its own) looking like a network-wide
494
+ * event — a real local-network blip surfaces across unrelated hosts at
495
+ * once.
496
+ */
497
+ readonly hostThreshold: number;
498
+ }
499
+ /**
500
+ * One observed error, as fed to `NetworkOutageDetector.record`.
501
+ */
502
+ export interface NetworkErrorRecord {
503
+ readonly kind: ErrorKind;
504
+ readonly host: string;
505
+ /**
506
+ * Epoch ms this error was observed. Caller-supplied — the detector never
507
+ * calls `Date.now()` itself, so window-boundary behaviour can be pinned
508
+ * with exact values instead of fake timers. Callers MUST supply
509
+ * non-decreasing values across successive `record()` calls; the window
510
+ * eviction is a simple cutoff against the latest `at` and does not
511
+ * re-sort out-of-order input.
512
+ */
513
+ readonly at: number;
514
+ }
515
+ /**
516
+ * Emitted by `NetworkOutageDetector.record` the instant the sliding window
517
+ * crosses both thresholds.
518
+ */
519
+ export interface OutageSuspect {
520
+ /**
521
+ * Backdated to the earliest error still inside the window at trigger
522
+ * time — NOT the trigger instant itself. A sliding-window detector only
523
+ * confirms an outage after `W` seconds and `N` errors have accumulated,
524
+ * so the outage itself started earlier; backdating lets the persisted
525
+ * `network_outages` row (and the failures it retroactively covers) reach
526
+ * back to that earlier point instead of losing everything the detector
527
+ * missed while still accumulating evidence.
528
+ */
529
+ readonly startedAt: number;
530
+ /** The `at` of the record that tripped the threshold. */
531
+ readonly detectedAt: number;
532
+ /** Window size at trigger time (== `errorThreshold` or more). */
533
+ readonly triggerErrorCount: number;
534
+ /** Distinct host count at trigger time (== `hostThreshold` or more). */
535
+ readonly triggerHostCount: number;
352
536
  }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from './archive/types.js';
2
+ import type { NetworkProbe } from './crawler/probe-network.js';
2
3
  import type { InventoryMode } from './crawler/types.js';
3
4
  import type { CrawlEvent } from './types.js';
4
5
  import type { ExURL } from '@d-zero/shared/parse-url';
@@ -48,6 +49,38 @@ interface CrawlConfig extends Config {
48
49
  * this `null` so new rows are labelled `'crawled'` by the DB DEFAULT.
49
50
  */
50
51
  inventoryMode: InventoryMode | null;
52
+ /**
53
+ * See {@link CrawlerOptions.networkOutageWindowMs}. Omitted (`undefined`
54
+ * on the `Partial<CrawlConfig>` callers actually pass) falls through to
55
+ * `Crawler`'s own default — this field exists so tests can shrink the
56
+ * window for a fast, deterministic outage-detection cycle.
57
+ */
58
+ networkOutageWindowMs: number;
59
+ /** See {@link CrawlerOptions.networkOutageErrorThreshold}. */
60
+ networkOutageErrorThreshold: number;
61
+ /** See {@link CrawlerOptions.networkOutageHostThreshold}. */
62
+ networkOutageHostThreshold: number;
63
+ /** See {@link CrawlerOptions.networkOutageProbeIntervalMs}. */
64
+ networkOutageProbeIntervalMs: number;
65
+ /**
66
+ * See {@link CrawlerOptions.networkProbe}. The seam tests use to simulate
67
+ * confirmed outages and recoveries deterministically without touching
68
+ * the real network — plumbed through from `CrawlerOrchestrator.crawling`'s
69
+ * `options` so an E2E test can inject it via the public API.
70
+ */
71
+ networkProbe: NetworkProbe | null;
72
+ /** See {@link CrawlerOptions.dedupeCap}. `null`/omitted disables the feature. */
73
+ dedupeCap: number | null;
74
+ /** See {@link CrawlerOptions.dedupeMapCap}. Omitted falls through to `Crawler`'s own default. */
75
+ dedupeMapCap: number;
76
+ /**
77
+ * See {@link CrawlerOptions.preloadedStickyShapeKeys}. Set internally by
78
+ * the four resuming-session static methods
79
+ * (`append`/`inventory`/`retryFailed`/`resume`), each independently
80
+ * calling `archive.listDedupeCapShapeKeys()`; not part of the public
81
+ * options a caller of those methods passes directly.
82
+ */
83
+ preloadedStickyShapeKeys: readonly string[];
51
84
  }
52
85
  /**
53
86
  * Callback invoked after the CrawlerOrchestrator instance is fully initialized
@@ -56,6 +89,20 @@ interface CrawlConfig extends Config {
56
89
  * @param config - The resolved archive configuration.
57
90
  */
58
91
  type CrawlInitializedCallback = (orchestrator: CrawlerOrchestrator, config: Config) => void | Promise<void>;
92
+ /**
93
+ * The CLI's already-read `--inventory` source list, passed to
94
+ * {@link CrawlerOrchestrator.inventory} instead of a file path — see that
95
+ * method's `source` param for why the path itself never crosses this
96
+ * boundary.
97
+ */
98
+ interface InventorySource {
99
+ /** Lower-case hex SHA-256 digest of `bytes` (`computeFileSha256(bytes)`). */
100
+ sha256: string;
101
+ /** The exact bytes of the source list file, archived verbatim. */
102
+ bytes: Buffer;
103
+ /** Number of source-file lines the CLI warned-and-dropped for failing URL validation, before `inventoryUrls` was ever built. Recorded on the audit row as `inventory_runs.invalid_skipped`. */
104
+ invalidLineCount: number;
105
+ }
59
106
  /**
60
107
  * The main entry point for Nitpicker web crawling and archiving.
61
108
  *
@@ -184,22 +231,29 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
184
231
  * 3. Reject archives with unfinished `pending` URLs — those would inherit
185
232
  * the inventory `source` label by mistake. Operator must resume /
186
233
  * retry-failed first.
187
- * 4. Parse the URL list. Anything outside the archived scope is warned
188
- * and skipped (inventory is per-server by design).
189
- * 5. Subtract URLs that already exist in `pages` or `resources` so the
234
+ * 4. If `source` is given, archive its exact bytes under
235
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
236
+ * Done before scope classification so even a run that discards every
237
+ * URL (out of scope or already known) still leaves a recoverable
238
+ * copy of what was fed in.
239
+ * 5. Parse the URL list — the CLI has already warned-and-dropped
240
+ * unparseable-URL lines before calling this method, so every
241
+ * remaining entry parses. Anything outside the archived scope is
242
+ * warned and skipped (inventory is per-server by design).
243
+ * 6. Subtract URLs that already exist in `pages` or `resources` so the
190
244
  * second (and N-th) inventory pass is a no-op for known rows — keeps
191
245
  * `'inventory-seed'` rows from being silently demoted.
192
- * 6. Make `<archive>.bak`. Anything thrown beyond this point restores
246
+ * 7. Make `<archive>.bak`. Anything thrown beyond this point restores
193
247
  * from the backup.
194
- * 7. HEAD-probe each novel URL. Responses classified as HTML are queued
248
+ * 8. HEAD-probe each novel URL. Responses classified as HTML are queued
195
249
  * as Crawler seeds (`'inventory-seed'`); everything else is recorded
196
250
  * in `resources` directly as `'inventory-seed'` (no browser launch).
197
- * 8. If any HTML seeds exist, start a Crawler with
251
+ * 9. If any HTML seeds exist, start a Crawler with
198
252
  * `inventoryMode = { seedUrls }` so the rendered page and every newly
199
253
  * discovered downstream link is labelled correctly. `resume` is fed
200
254
  * the existing `scraped` / `resources` sets so links into already-
201
255
  * crawled pages stop at the seen-gate without re-rendering.
202
- * 9. Drop the backup on success; restore it on any throw.
256
+ * 10. Drop the backup on success; restore it on any throw.
203
257
  *
204
258
  * Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
205
259
  * `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
@@ -209,20 +263,22 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
209
263
  * @param inventoryUrls - Pre-read URL list (one URL per element).
210
264
  * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
211
265
  * @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
212
- * @param sourceFileSha256 - **Pre-computed** SHA-256 hex digest of the
213
- * source URL list. The orchestrator deliberately does NOT receive
214
- * the file path: the path is privacy-sensitive (leaks user-home /
215
- * OS structure when archives are shared) and we want it lifted off
216
- * this boundary so no future log line / breadcrumb / error message
217
- * inside the orchestrator can accidentally re-leak it. The CLI
218
- * computes the digest via `computeFileSha256(resolvedListFile)`
219
- * and passes it through here. Pass `null` for programmatic
220
- * callers that built `inventoryUrls` in-memory; the audit row's
221
- * `source_file_sha256` column will be `NULL`.
266
+ * @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
267
+ * The orchestrator deliberately does NOT receive the file path: the
268
+ * path is privacy-sensitive (leaks user-home / OS structure when
269
+ * archives are shared) and we want it lifted off this boundary so no
270
+ * future log line / breadcrumb / error message inside the orchestrator
271
+ * can accidentally re-leak it. `bytes` is archived verbatim under
272
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
273
+ * before scope classification, so a later `--inventory` run against
274
+ * the same list is an audit no-op even when it discards zero new
275
+ * URLs. Pass `null` for programmatic callers that built
276
+ * `inventoryUrls` in-memory; the audit row's `source_file_sha256`
277
+ * column will be `NULL` and no source list is archived.
222
278
  * @returns The orchestrator instance after a successful inventory pass.
223
279
  * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
224
280
  */
225
- static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, sourceFileSha256?: string | null): Promise<CrawlerOrchestrator>;
281
+ static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, source?: InventorySource | null): Promise<CrawlerOrchestrator>;
226
282
  /**
227
283
  * Re-fetch previously-failed pages in an existing `.nitpicker` archive.
228
284
  *