@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
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Level-triggered gate that dealer worker callbacks await before doing any
3
+ * network work. Achieves "pause the crawl until the network recovers"
4
+ * without touching `@d-zero/dealer` internals — a closed gate just makes
5
+ * the worker's task body a long-running `await`, which is all dealer ever
6
+ * sees.
7
+ *
8
+ * **`epoch`** counts outage generations: it increments on every `close()`
9
+ * transition (open → closed) and holds that value for the remainder of the
10
+ * closed period AND through the following open period, until the next
11
+ * `close()`. This is the hook `destinationCache` / `dnsBurnedHostCache`
12
+ * eviction uses: an entry written while the gate was closed is tagged with
13
+ * `gate.epoch` at write time; after `open()`, the caller purges entries
14
+ * tagged with that same epoch value. Because `epoch` only advances on
15
+ * `close()` (never on `open()`), entries written during ordinary (open)
16
+ * operation are never mistakenly tagged with a soon-to-be-purged epoch —
17
+ * only writes that happen while `isOpen` is `false` get tagged at all.
18
+ */
19
+ export default class NetworkGate {
20
+ #epoch = 0;
21
+ #open = true;
22
+ #waiters = [];
23
+ /** `true` when the gate is open (the default). */
24
+ get isOpen() {
25
+ return this.#open;
26
+ }
27
+ /**
28
+ * Current outage-generation counter. See the class docstring for how
29
+ * cache-eviction callers are expected to use this value.
30
+ */
31
+ get epoch() {
32
+ return this.#epoch;
33
+ }
34
+ /**
35
+ * Close the gate, blocking future {@link wait} callers until the next
36
+ * {@link open}. Idempotent — calling `close()` while already closed does
37
+ * nothing (in particular, it does NOT bump {@link epoch} a second time,
38
+ * which would otherwise let a still-open outage's cache entries escape
39
+ * eviction under a stale epoch number).
40
+ */
41
+ close() {
42
+ if (!this.#open) {
43
+ return;
44
+ }
45
+ this.#open = false;
46
+ this.#epoch += 1;
47
+ }
48
+ /**
49
+ * Open the gate, resolving every {@link wait} caller currently pending —
50
+ * including ones that subscribed after `close()` but before this call.
51
+ * Idempotent — calling `open()` while already open does nothing.
52
+ */
53
+ open() {
54
+ if (this.#open) {
55
+ return;
56
+ }
57
+ this.#open = true;
58
+ const waiters = this.#waiters;
59
+ this.#waiters = [];
60
+ for (const resolve of waiters) {
61
+ resolve();
62
+ }
63
+ }
64
+ /**
65
+ * Resolve immediately if the gate is open; otherwise resolve on the next
66
+ * {@link open}. Multiple concurrent callers all resolve on the same
67
+ * `open()` call — no awaiter is dropped regardless of when it subscribed
68
+ * relative to others.
69
+ */
70
+ wait() {
71
+ if (this.#open) {
72
+ return Promise.resolve();
73
+ }
74
+ return new Promise((resolve) => {
75
+ this.#waiters.push(resolve);
76
+ });
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ import type { NetworkErrorRecord, NetworkOutageDetectorOptions, OutageSuspect } from './types.js';
2
+ /**
3
+ * Sliding-window detector for "this looks like the crawl operator's own
4
+ * network, not the target sites, is having a bad time" — the trigger that
5
+ * hands off to an active probe (see `probe-network.ts`) before the crawler
6
+ * commits to pausing.
7
+ *
8
+ * Deliberately dependency-free: no timers, no DB, no `Crawler` instance.
9
+ * Time flows in via {@link NetworkErrorRecord.at} only, which is what makes
10
+ * window-boundary behaviour testable with exact values instead of
11
+ * `vi.useFakeTimers()`.
12
+ * @example
13
+ * ```ts
14
+ * const detector = new NetworkOutageDetector({
15
+ * windowMs: 10_000,
16
+ * errorThreshold: 5,
17
+ * hostThreshold: 2,
18
+ * });
19
+ * const suspect = detector.record({ kind: 'dns', host: 'a.example', at: Date.now() });
20
+ * if (suspect) {
21
+ * // probe before closing the gate
22
+ * }
23
+ * ```
24
+ */
25
+ export default class NetworkOutageDetector {
26
+ #private;
27
+ constructor(options: NetworkOutageDetectorOptions);
28
+ /**
29
+ * Record one observed error and check whether it tips the sliding window
30
+ * over both thresholds.
31
+ *
32
+ * Non-network `kind`s (see `NETWORK_RELATED_ERROR_KINDS`) are silently
33
+ * ignored: they never enter the window and can never contribute to a
34
+ * trigger, regardless of how many arrive.
35
+ * @param record - The observed error.
36
+ * @param record.kind
37
+ * @param record.host
38
+ * @param record.at
39
+ * @returns An {@link OutageSuspect} the instant both thresholds are met,
40
+ * or `null` otherwise. On a trigger, the window is cleared so the same
41
+ * batch of errors cannot fire a second time on the next call.
42
+ */
43
+ record({ kind, host, at }: NetworkErrorRecord): OutageSuspect | null;
44
+ /**
45
+ * Clear the sliding window. Called at the start of a fresh crawl session
46
+ * (`Crawler.#runDeal`) so error observations from a prior session on the
47
+ * same `Crawler` instance never carry over — mirrors `#successfulHosts
48
+ * .clear()` / `#scrapedDestinations.clear()`'s per-session reset.
49
+ */
50
+ reset(): void;
51
+ }
@@ -0,0 +1,81 @@
1
+ import { NETWORK_RELATED_ERROR_KINDS } from '../network-related-error-kinds.js';
2
+ /**
3
+ * Sliding-window detector for "this looks like the crawl operator's own
4
+ * network, not the target sites, is having a bad time" — the trigger that
5
+ * hands off to an active probe (see `probe-network.ts`) before the crawler
6
+ * commits to pausing.
7
+ *
8
+ * Deliberately dependency-free: no timers, no DB, no `Crawler` instance.
9
+ * Time flows in via {@link NetworkErrorRecord.at} only, which is what makes
10
+ * window-boundary behaviour testable with exact values instead of
11
+ * `vi.useFakeTimers()`.
12
+ * @example
13
+ * ```ts
14
+ * const detector = new NetworkOutageDetector({
15
+ * windowMs: 10_000,
16
+ * errorThreshold: 5,
17
+ * hostThreshold: 2,
18
+ * });
19
+ * const suspect = detector.record({ kind: 'dns', host: 'a.example', at: Date.now() });
20
+ * if (suspect) {
21
+ * // probe before closing the gate
22
+ * }
23
+ * ```
24
+ */
25
+ export default class NetworkOutageDetector {
26
+ #entries = [];
27
+ #errorThreshold;
28
+ #hostThreshold;
29
+ #windowMs;
30
+ constructor(options) {
31
+ this.#windowMs = options.windowMs;
32
+ this.#errorThreshold = options.errorThreshold;
33
+ this.#hostThreshold = options.hostThreshold;
34
+ }
35
+ /**
36
+ * Record one observed error and check whether it tips the sliding window
37
+ * over both thresholds.
38
+ *
39
+ * Non-network `kind`s (see `NETWORK_RELATED_ERROR_KINDS`) are silently
40
+ * ignored: they never enter the window and can never contribute to a
41
+ * trigger, regardless of how many arrive.
42
+ * @param record - The observed error.
43
+ * @param record.kind
44
+ * @param record.host
45
+ * @param record.at
46
+ * @returns An {@link OutageSuspect} the instant both thresholds are met,
47
+ * or `null` otherwise. On a trigger, the window is cleared so the same
48
+ * batch of errors cannot fire a second time on the next call.
49
+ */
50
+ record({ kind, host, at }) {
51
+ if (!NETWORK_RELATED_ERROR_KINDS.has(kind)) {
52
+ return null;
53
+ }
54
+ const cutoff = at - this.#windowMs;
55
+ this.#entries = this.#entries.filter((entry) => entry.at >= cutoff);
56
+ this.#entries.push({ host, at });
57
+ const distinctHosts = new Set(this.#entries.map((entry) => entry.host)).size;
58
+ if (this.#entries.length < this.#errorThreshold ||
59
+ distinctHosts < this.#hostThreshold) {
60
+ return null;
61
+ }
62
+ const startedAt = Math.min(...this.#entries.map((entry) => entry.at));
63
+ const suspect = {
64
+ startedAt,
65
+ detectedAt: at,
66
+ triggerErrorCount: this.#entries.length,
67
+ triggerHostCount: distinctHosts,
68
+ };
69
+ this.#entries = [];
70
+ return suspect;
71
+ }
72
+ /**
73
+ * Clear the sliding window. Called at the start of a fresh crawl session
74
+ * (`Crawler.#runDeal`) so error observations from a prior session on the
75
+ * same `Crawler` instance never carry over — mirrors `#successfulHosts
76
+ * .clear()` / `#scrapedDestinations.clear()`'s per-session reset.
77
+ */
78
+ reset() {
79
+ this.#entries = [];
80
+ }
81
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Session-scoped counter for how many operator-network outages were
3
+ * confirmed, and their total duration. The orchestrator accumulates into
4
+ * this at every `networkOutageRecovered` event, reads it at crawl shutdown
5
+ * to emit a `[network] N outage(s), Ms total` summary line, then zeroes it
6
+ * for the next session (mirrors `dnsBurnedHostShortCircuitCounter`'s
7
+ * lifecycle, including its known limitation: two `CrawlerOrchestrator`
8
+ * crawls running concurrently in the same process would accumulate into and
9
+ * zero the same shared counter, cross-contaminating each other's summary.
10
+ * The CLI only ever runs one crawl per process, so this has never been
11
+ * observed in practice; fixing it would mean threading per-session state
12
+ * through both counters together, out of scope here).
13
+ *
14
+ * Exposed as a mutable object (not a plain `let`) so the counter remains a
15
+ * single shared reference across crawler / orchestrator imports — `let`
16
+ * bindings cannot be mutated from another module.
17
+ */
18
+ export declare const networkOutageSummaryCounter: {
19
+ confirmedCount: number;
20
+ totalDurationMs: number;
21
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Session-scoped counter for how many operator-network outages were
3
+ * confirmed, and their total duration. The orchestrator accumulates into
4
+ * this at every `networkOutageRecovered` event, reads it at crawl shutdown
5
+ * to emit a `[network] N outage(s), Ms total` summary line, then zeroes it
6
+ * for the next session (mirrors `dnsBurnedHostShortCircuitCounter`'s
7
+ * lifecycle, including its known limitation: two `CrawlerOrchestrator`
8
+ * crawls running concurrently in the same process would accumulate into and
9
+ * zero the same shared counter, cross-contaminating each other's summary.
10
+ * The CLI only ever runs one crawl per process, so this has never been
11
+ * observed in practice; fixing it would mean threading per-session state
12
+ * through both counters together, out of scope here).
13
+ *
14
+ * Exposed as a mutable object (not a plain `let`) so the counter remains a
15
+ * single shared reference across crawler / orchestrator imports — `let`
16
+ * bindings cannot be mutated from another module.
17
+ */
18
+ export const networkOutageSummaryCounter = { confirmedCount: 0, totalDurationMs: 0 };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Function shape for an injectable network-reachability probe: given a
3
+ * hostname, resolve to `true` if the network path to it appears healthy,
4
+ * `false` otherwise. Never rejects — a probe failure is a normal outcome
5
+ * (that's the whole point of probing), not an exceptional one.
6
+ *
7
+ * Injected via `CrawlerOptions.networkProbe` so tests can simulate
8
+ * confirmed outages and recoveries deterministically (a function that fails
9
+ * N times then starts succeeding) without touching the real network — the
10
+ * same injection pattern as `ResourceLookup` / `PageSourceLookup`.
11
+ */
12
+ export type NetworkProbe = (host: string) => Promise<boolean>;
13
+ /**
14
+ * Default {@link NetworkProbe}: an active `dns.lookup` against `host`.
15
+ *
16
+ * Deliberately does not hit a hardcoded external address (e.g. `1.1.1.1`) —
17
+ * probing a host that has already answered successfully during this crawl
18
+ * session (see `choose-probe-host.ts`) avoids depending on infrastructure
19
+ * outside the sites actually being crawled. `dns.lookup` alone is enough:
20
+ * it exercises the operator's local resolver / network path, which is
21
+ * exactly the layer an operator-side outage breaks.
22
+ * @param host - Hostname to resolve.
23
+ * @returns `true` if the lookup succeeds, `false` on any error (including
24
+ * NXDOMAIN, timeout, or resolver unavailability).
25
+ */
26
+ export declare function probeNetwork(host: string): Promise<boolean>;
@@ -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
+ }
@@ -54,6 +54,8 @@ export function resourceToPageData(params) {
54
54
  anchorList: [],
55
55
  imageList: [],
56
56
  html: '',
57
+ mainContents: null,
58
+ scrollHeight: null,
57
59
  isSkipped: false,
58
60
  };
59
61
  }
@@ -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
@@ -129,6 +131,11 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
129
131
  userAgent: string;
130
132
  /** Whether to ignore robots.txt restrictions. */
131
133
  ignoreRobots: boolean;
134
+ /**
135
+ * CSS selector overriding beholder's automatic main-content-region
136
+ * detection, or `null`/undefined to use the automatic heuristic.
137
+ */
138
+ mainContentSelector?: string | null;
132
139
  /**
133
140
  * Lookup for previously captured sub-resources, or `null` to disable the
134
141
  * resource-reuse optimization. See {@link ResourceLookup}.
@@ -155,6 +162,34 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
155
162
  * the DB DEFAULT `'crawled'` applies.
156
163
  */
157
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;
158
193
  }
159
194
  /**
160
195
  * Inventory-mode runtime configuration. Passed from
@@ -289,6 +324,31 @@ export interface CrawlerEventTypes {
289
324
  /** The URL of the referenced resource (without hash). */
290
325
  src: string;
291
326
  };
327
+ /**
328
+ * Emitted once per scrape with the console messages / page errors
329
+ * beholder captured for that page (issue #228). Only emitted when
330
+ * `entries` is non-empty — see `Crawler#handleConsoleLogs` for why a
331
+ * degraded re-scrape that captures nothing must not clear prior good
332
+ * data.
333
+ */
334
+ consoleLogs: {
335
+ /**
336
+ * The originally-requested URL, normalised (`withoutHashAndAuth`
337
+ * form) — the same identity `updatePage` resolves its redirect
338
+ * chain from, NOT necessarily the page that ends up holding the
339
+ * content.
340
+ */
341
+ pageUrl: string;
342
+ /**
343
+ * The redirect chain hops captured during fetch, in order. Empty
344
+ * when the page was not redirected, or when the scrape produced no
345
+ * `pageData` (a `'skipped'` / `'error'` result) and no redirect
346
+ * information is available.
347
+ */
348
+ redirectPaths: readonly string[];
349
+ /** The captured console messages / page errors, in capture order. */
350
+ entries: ConsoleLogEntry[];
351
+ };
292
352
  /**
293
353
  * Emitted when the entire crawl process has completed or been aborted.
294
354
  */
@@ -344,4 +404,95 @@ export interface CrawlerEventTypes {
344
404
  */
345
405
  source: PageSource | undefined;
346
406
  };
407
+ /**
408
+ * Emitted the instant `Crawler` closes its internal network gate after a
409
+ * recovery probe CONFIRMS a suspect outage (the sliding-window threshold
410
+ * alone only makes it a suspect — see `NetworkOutageDetector`). The
411
+ * orchestrator persists this via `Archive.insertNetworkOutage` and must
412
+ * remember the returned row id to pass to the matching
413
+ * `networkOutageRecovered` event, since `Crawler` itself never touches
414
+ * the archive and has no way to know the row's id.
415
+ */
416
+ networkOutageConfirmed: {
417
+ /** Backdated to the earliest error still inside the detector's window at trigger time. */
418
+ startedAt: number;
419
+ /** When the sliding window actually crossed both thresholds. */
420
+ detectedAt: number;
421
+ /** Hostname the recovery probe is targeting, or `null` if none was available. */
422
+ probeHost: string | null;
423
+ triggerErrorCount: number;
424
+ triggerHostCount: number;
425
+ };
426
+ /**
427
+ * Emitted the instant `Crawler` reopens its internal network gate after
428
+ * a recovery probe succeeds. NOT emitted when the gate is opened
429
+ * because the crawl was aborted while paused — in that case the outage
430
+ * row is deliberately left open for the next writer session's
431
+ * boot-time finalizer to resolve (see
432
+ * `db-ops/outages/close-stale-open-network-outages.ts`), since an abort
433
+ * says nothing about whether the network actually recovered.
434
+ */
435
+ networkOutageRecovered: {
436
+ /** Epoch ms the recovery probe first succeeded. */
437
+ endedAt: number;
438
+ };
439
+ }
440
+ /**
441
+ * Tunables for `NetworkOutageDetector`.
442
+ */
443
+ export interface NetworkOutageDetectorOptions {
444
+ /**
445
+ * Sliding-window size in ms (`W`). Before each check, entries older than
446
+ * `at - windowMs` (inclusive boundary — an entry exactly `windowMs` old
447
+ * still counts) are evicted.
448
+ */
449
+ readonly windowMs: number;
450
+ /** Minimum error count within the window to declare a suspect outage (`N`). */
451
+ readonly errorThreshold: number;
452
+ /**
453
+ * Minimum number of DISTINCT hosts represented in the window to declare
454
+ * a suspect outage (`M`). Guards against one flaky host (a site that is
455
+ * genuinely retrying/failing on its own) looking like a network-wide
456
+ * event — a real local-network blip surfaces across unrelated hosts at
457
+ * once.
458
+ */
459
+ readonly hostThreshold: number;
460
+ }
461
+ /**
462
+ * One observed error, as fed to `NetworkOutageDetector.record`.
463
+ */
464
+ export interface NetworkErrorRecord {
465
+ readonly kind: ErrorKind;
466
+ readonly host: string;
467
+ /**
468
+ * Epoch ms this error was observed. Caller-supplied — the detector never
469
+ * calls `Date.now()` itself, so window-boundary behaviour can be pinned
470
+ * with exact values instead of fake timers. Callers MUST supply
471
+ * non-decreasing values across successive `record()` calls; the window
472
+ * eviction is a simple cutoff against the latest `at` and does not
473
+ * re-sort out-of-order input.
474
+ */
475
+ readonly at: number;
476
+ }
477
+ /**
478
+ * Emitted by `NetworkOutageDetector.record` the instant the sliding window
479
+ * crosses both thresholds.
480
+ */
481
+ export interface OutageSuspect {
482
+ /**
483
+ * Backdated to the earliest error still inside the window at trigger
484
+ * time — NOT the trigger instant itself. A sliding-window detector only
485
+ * confirms an outage after `W` seconds and `N` errors have accumulated,
486
+ * so the outage itself started earlier; backdating lets the persisted
487
+ * `network_outages` row (and the failures it retroactively covers) reach
488
+ * back to that earlier point instead of losing everything the detector
489
+ * missed while still accumulating evidence.
490
+ */
491
+ readonly startedAt: number;
492
+ /** The `at` of the record that tripped the threshold. */
493
+ readonly detectedAt: number;
494
+ /** Window size at trigger time (== `errorThreshold` or more). */
495
+ readonly triggerErrorCount: number;
496
+ /** Distinct host count at trigger time (== `hostThreshold` or more). */
497
+ readonly triggerHostCount: number;
347
498
  }
@@ -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,26 @@ 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;
51
72
  }
52
73
  /**
53
74
  * Callback invoked after the CrawlerOrchestrator instance is fully initialized
@@ -56,6 +77,20 @@ interface CrawlConfig extends Config {
56
77
  * @param config - The resolved archive configuration.
57
78
  */
58
79
  type CrawlInitializedCallback = (orchestrator: CrawlerOrchestrator, config: Config) => void | Promise<void>;
80
+ /**
81
+ * The CLI's already-read `--inventory` source list, passed to
82
+ * {@link CrawlerOrchestrator.inventory} instead of a file path — see that
83
+ * method's `source` param for why the path itself never crosses this
84
+ * boundary.
85
+ */
86
+ interface InventorySource {
87
+ /** Lower-case hex SHA-256 digest of `bytes` (`computeFileSha256(bytes)`). */
88
+ sha256: string;
89
+ /** The exact bytes of the source list file, archived verbatim. */
90
+ bytes: Buffer;
91
+ /** 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`. */
92
+ invalidLineCount: number;
93
+ }
59
94
  /**
60
95
  * The main entry point for Nitpicker web crawling and archiving.
61
96
  *
@@ -184,22 +219,29 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
184
219
  * 3. Reject archives with unfinished `pending` URLs — those would inherit
185
220
  * the inventory `source` label by mistake. Operator must resume /
186
221
  * 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
222
+ * 4. If `source` is given, archive its exact bytes under
223
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList}).
224
+ * Done before scope classification so even a run that discards every
225
+ * URL (out of scope or already known) still leaves a recoverable
226
+ * copy of what was fed in.
227
+ * 5. Parse the URL list — the CLI has already warned-and-dropped
228
+ * unparseable-URL lines before calling this method, so every
229
+ * remaining entry parses. Anything outside the archived scope is
230
+ * warned and skipped (inventory is per-server by design).
231
+ * 6. Subtract URLs that already exist in `pages` or `resources` so the
190
232
  * second (and N-th) inventory pass is a no-op for known rows — keeps
191
233
  * `'inventory-seed'` rows from being silently demoted.
192
- * 6. Make `<archive>.bak`. Anything thrown beyond this point restores
234
+ * 7. Make `<archive>.bak`. Anything thrown beyond this point restores
193
235
  * from the backup.
194
- * 7. HEAD-probe each novel URL. Responses classified as HTML are queued
236
+ * 8. HEAD-probe each novel URL. Responses classified as HTML are queued
195
237
  * as Crawler seeds (`'inventory-seed'`); everything else is recorded
196
238
  * in `resources` directly as `'inventory-seed'` (no browser launch).
197
- * 8. If any HTML seeds exist, start a Crawler with
239
+ * 9. If any HTML seeds exist, start a Crawler with
198
240
  * `inventoryMode = { seedUrls }` so the rendered page and every newly
199
241
  * discovered downstream link is labelled correctly. `resume` is fed
200
242
  * the existing `scraped` / `resources` sets so links into already-
201
243
  * crawled pages stop at the seen-gate without re-rendering.
202
- * 9. Drop the backup on success; restore it on any throw.
244
+ * 10. Drop the backup on success; restore it on any throw.
203
245
  *
204
246
  * Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
205
247
  * `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
@@ -209,20 +251,22 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
209
251
  * @param inventoryUrls - Pre-read URL list (one URL per element).
210
252
  * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
211
253
  * @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`.
254
+ * @param source - The CLI's already-read source list, as `{ sha256, bytes }`.
255
+ * The orchestrator deliberately does NOT receive the file path: the
256
+ * path is privacy-sensitive (leaks user-home / OS structure when
257
+ * archives are shared) and we want it lifted off this boundary so no
258
+ * future log line / breadcrumb / error message inside the orchestrator
259
+ * can accidentally re-leak it. `bytes` is archived verbatim under
260
+ * `inventory/<sha256>.txt` (see {@link Archive.saveInventorySourceList})
261
+ * before scope classification, so a later `--inventory` run against
262
+ * the same list is an audit no-op even when it discards zero new
263
+ * URLs. Pass `null` for programmatic callers that built
264
+ * `inventoryUrls` in-memory; the audit row's `source_file_sha256`
265
+ * column will be `NULL` and no source list is archived.
222
266
  * @returns The orchestrator instance after a successful inventory pass.
223
267
  * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
224
268
  */
225
- static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, sourceFileSha256?: string | null): Promise<CrawlerOrchestrator>;
269
+ static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, source?: InventorySource | null): Promise<CrawlerOrchestrator>;
226
270
  /**
227
271
  * Re-fetch previously-failed pages in an existing `.nitpicker` archive.
228
272
  *