@nitpicker/crawler 0.9.0 → 0.12.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 (148) hide show
  1. package/lib/archive/archive-accessor.d.ts +87 -14
  2. package/lib/archive/archive-accessor.js +162 -36
  3. package/lib/archive/archive.d.ts +262 -24
  4. package/lib/archive/archive.js +397 -86
  5. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  6. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  7. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  8. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  9. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  10. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  11. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  12. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  13. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  14. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  15. package/lib/archive/database.d.ts +411 -26
  16. package/lib/archive/database.js +1881 -540
  17. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  18. package/lib/archive/derive-lineage-from-parent.js +42 -0
  19. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
  20. package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
  21. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  22. package/lib/archive/get-failed-page-messages.js +131 -0
  23. package/lib/archive/init-schema.d.ts +53 -2
  24. package/lib/archive/init-schema.js +399 -15
  25. package/lib/archive/is-inventory-source.d.ts +21 -0
  26. package/lib/archive/is-inventory-source.js +22 -0
  27. package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
  28. package/lib/archive/meta/assert-compatible-version.js +72 -0
  29. package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
  30. package/lib/archive/meta/classify-jsonld-type.js +43 -0
  31. package/lib/archive/meta/compare-semver.d.ts +23 -0
  32. package/lib/archive/meta/compare-semver.js +51 -0
  33. package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
  34. package/lib/archive/meta/compute-page-denormalized.js +35 -0
  35. package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
  36. package/lib/archive/meta/derive-flat-from-meta.js +158 -0
  37. package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
  38. package/lib/archive/meta/derive-meta-extras.js +23 -0
  39. package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
  40. package/lib/archive/meta/extract-tags-for-archive.js +36 -0
  41. package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
  42. package/lib/archive/meta/summarize-jsonld.js +29 -0
  43. package/lib/archive/meta/summarize-tags.d.ts +16 -0
  44. package/lib/archive/meta/summarize-tags.js +33 -0
  45. package/lib/archive/meta/types.d.ts +207 -0
  46. package/lib/archive/meta/types.js +33 -0
  47. package/lib/archive/migrate-crawl-errors.d.ts +20 -0
  48. package/lib/archive/migrate-crawl-errors.js +38 -0
  49. package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
  50. package/lib/archive/migrate-html-blob-tables.js +53 -0
  51. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  52. package/lib/archive/migrate-inventory-runs.js +52 -0
  53. package/lib/archive/migrate-page-errors.d.ts +16 -0
  54. package/lib/archive/migrate-page-errors.js +35 -0
  55. package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
  56. package/lib/archive/migrate-pages-resources-source.js +46 -0
  57. package/lib/archive/page.d.ts +187 -49
  58. package/lib/archive/page.js +258 -63
  59. package/lib/archive/peek-archive-lock.d.ts +40 -0
  60. package/lib/archive/peek-archive-lock.js +62 -0
  61. package/lib/archive/resolve-redirect-chain.d.ts +33 -0
  62. package/lib/archive/resolve-redirect-chain.js +27 -0
  63. package/lib/archive/types.d.ts +168 -26
  64. package/lib/classify-error-kind.d.ts +19 -0
  65. package/lib/classify-error-kind.js +122 -0
  66. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  67. package/lib/crawler/build-js-redirect-edge.js +57 -0
  68. package/lib/crawler/build-redirect-event.d.ts +24 -0
  69. package/lib/crawler/build-redirect-event.js +28 -0
  70. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  71. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  72. package/lib/crawler/close-browser-safely.d.ts +64 -0
  73. package/lib/crawler/close-browser-safely.js +73 -0
  74. package/lib/crawler/crawler.d.ts +7 -2
  75. package/lib/crawler/crawler.js +919 -113
  76. package/lib/crawler/create-change-phase-handler.d.ts +54 -0
  77. package/lib/crawler/create-change-phase-handler.js +44 -0
  78. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  79. package/lib/crawler/derive-js-redirect-target.js +129 -0
  80. package/lib/crawler/derive-page-source.d.ts +23 -0
  81. package/lib/crawler/derive-page-source.js +28 -0
  82. package/lib/crawler/derive-resource-source.d.ts +33 -0
  83. package/lib/crawler/derive-resource-source.js +37 -0
  84. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  85. package/lib/crawler/dns-burned-host-cache.js +25 -0
  86. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  87. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  88. package/lib/crawler/drain-phase-errors.d.ts +48 -0
  89. package/lib/crawler/drain-phase-errors.js +35 -0
  90. package/lib/crawler/fetch-destination.d.ts +12 -4
  91. package/lib/crawler/fetch-destination.js +132 -18
  92. package/lib/crawler/format-crawl-progress.d.ts +12 -3
  93. package/lib/crawler/format-crawl-progress.js +14 -6
  94. package/lib/crawler/handle-browser-close.d.ts +29 -0
  95. package/lib/crawler/handle-browser-close.js +28 -0
  96. package/lib/crawler/is-html-content-type.d.ts +17 -0
  97. package/lib/crawler/is-html-content-type.js +19 -0
  98. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  99. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  100. package/lib/crawler/is-likely-html-url.d.ts +22 -0
  101. package/lib/crawler/is-likely-html-url.js +65 -0
  102. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  103. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  104. package/lib/crawler/kill-process-tree.d.ts +94 -0
  105. package/lib/crawler/kill-process-tree.js +178 -0
  106. package/lib/crawler/link-list.d.ts +21 -1
  107. package/lib/crawler/link-list.js +25 -4
  108. package/lib/crawler/link-to-page-data.d.ts +13 -5
  109. package/lib/crawler/link-to-page-data.js +26 -5
  110. package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
  111. package/lib/crawler/log-undrained-phase-errors.js +34 -0
  112. package/lib/crawler/normalize-content-type.d.ts +14 -0
  113. package/lib/crawler/normalize-content-type.js +20 -0
  114. package/lib/crawler/partition-urls-by-html.d.ts +16 -0
  115. package/lib/crawler/partition-urls-by-html.js +23 -0
  116. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  117. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  118. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  119. package/lib/crawler/preload-short-circuit-error.js +25 -0
  120. package/lib/crawler/redirect-dest-key.d.ts +19 -0
  121. package/lib/crawler/redirect-dest-key.js +27 -0
  122. package/lib/crawler/resource-to-page-data.d.ts +28 -0
  123. package/lib/crawler/resource-to-page-data.js +59 -0
  124. package/lib/crawler/should-burn-host.d.ts +78 -0
  125. package/lib/crawler/should-burn-host.js +61 -0
  126. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  127. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  128. package/lib/crawler/types.d.ts +229 -1
  129. package/lib/crawler-orchestrator.d.ts +105 -3
  130. package/lib/crawler-orchestrator.js +620 -20
  131. package/lib/crawler.d.ts +8 -2
  132. package/lib/crawler.js +6 -1
  133. package/lib/permanent-error-kinds.d.ts +43 -0
  134. package/lib/permanent-error-kinds.js +48 -0
  135. package/lib/resource-row-to-lookup-result.d.ts +13 -0
  136. package/lib/resource-row-to-lookup-result.js +20 -0
  137. package/lib/types.d.ts +95 -1
  138. package/lib/utils/compute-file-sha256.d.ts +23 -0
  139. package/lib/utils/compute-file-sha256.js +55 -0
  140. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  141. package/lib/utils/error/emit-error-with-retry.js +44 -0
  142. package/lib/utils/error/emit-error.d.ts +39 -0
  143. package/lib/utils/error/emit-error.js +41 -0
  144. package/lib/utils/object/parse-response-headers.d.ts +12 -0
  145. package/lib/utils/object/parse-response-headers.js +26 -0
  146. package/package.json +11 -11
  147. package/lib/utils/error/error-emitter.d.ts +0 -18
  148. package/lib/utils/error/error-emitter.js +0 -29
@@ -1,6 +1,93 @@
1
+ import type { PageSource } from '../archive/types.js';
1
2
  import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
2
- import type { ChangePhaseEvent } from '@d-zero/beholder';
3
+ import type { ChangePhaseEvent, ScrapeResult } from '@d-zero/beholder';
3
4
  import type { ParseURLOptions } from '@d-zero/shared/parse-url';
5
+ /**
6
+ * Result of resolving a URL that redirects to a destination already rendered
7
+ * during this crawl (#73). The crawler records the redirect edge only and skips
8
+ * launching the browser, so the destination is never re-rendered.
9
+ */
10
+ export interface RedirectEdgeResult {
11
+ /** Discriminant marking this as a redirect-edge-only outcome. */
12
+ type: 'redirect-edge';
13
+ /** HEAD-resolved page data carrying the redirect chain (source → destination). */
14
+ pageData: PageData;
15
+ /**
16
+ * Where the chain came from. The caller needs this to decide whether the
17
+ * URLs in `pageData.redirectPaths` are already-known (HTTP chain — every
18
+ * hop was followed by the browser/HEAD pre-flight and the destination is
19
+ * already rendered) or brand-new (JS redirect — only the source was
20
+ * processed, the destination came out of `page.url()` and has never been
21
+ * touched).
22
+ *
23
+ * - `'http-chain'` — Returned when the HEAD pre-flight resolved a real
24
+ * 3xx chain and the destination has already been claimed via
25
+ * `#scrapedDestinations`. The crawler folds every URL in
26
+ * `redirectPaths` into the link-list's done-set; the destination is not
27
+ * re-enqueued because it is already in the archive.
28
+ * - `'js-redirect'` — Returned when `scraper.scrapeStart` threw because
29
+ * `page.goto()` resolved to `null` (client-side
30
+ * `window.location.replace()` / `<meta http-equiv="refresh">`) and
31
+ * `page.url()` exposed a different destination. `redirectPaths`
32
+ * contains exactly one URL: the JS-redirect target. The crawler MUST
33
+ * enqueue that destination so it reaches the browser, and MUST NOT
34
+ * fold it into the done-set (otherwise the dealer's `seen` rejects
35
+ * the push and the destination is silently lost from the archive).
36
+ */
37
+ source: 'http-chain' | 'js-redirect';
38
+ }
39
+ /**
40
+ * The outcome of {@link Crawler.#scrapePage}: either a normal scrape result from
41
+ * the browser/HEAD pipeline, or a {@link RedirectEdgeResult} when the URL's
42
+ * redirect destination was already rendered and only the edge needs recording.
43
+ */
44
+ export type ScrapeOutcome = ScrapeResult | RedirectEdgeResult;
45
+ /**
46
+ * Internal envelope returned by {@link Crawler.#launchBrowserAndScrape} that
47
+ * augments beholder's {@link ScrapeResult} with the puppeteer-side
48
+ * post-navigation URL.
49
+ *
50
+ * **Why:** when `scraper.scrapeStart` throws because `page.goto()` resolved to
51
+ * `null` (the classic puppeteer symptom of a client-side
52
+ * `window.location.replace()` / meta-refresh firing mid-navigation), the only
53
+ * authoritative source for the URL the browser actually landed on is
54
+ * `page.url()` — neither the HEAD pre-flight nor the thrown error carries it.
55
+ * Capturing it here lets `#scrapePage` fold the source into a redirect edge
56
+ * instead of recording a hard `status = -1` that `--retry-failed` would chase
57
+ * forever (`Page.goto returned null` classifies as `protocol`, which is neither
58
+ * permanent nor a puppeteer-fallback kind — so the SQL filter resets it every
59
+ * pass and the next pass replays the same failure).
60
+ *
61
+ * `postNavigationUrl` is optional because:
62
+ * - successful / skipped outcomes do not need it (the success path already
63
+ * exposes the final URL via `pageData.url` + `redirectPaths`);
64
+ * - capturing can itself fail when the underlying browser context is already
65
+ * torn down (target closed, session killed) — we treat that as "no extra
66
+ * information" and fall through to the existing error path.
67
+ */
68
+ export type BrowserScrapeResult = ScrapeResult & {
69
+ /**
70
+ * URL puppeteer reports via `page.url()` *after* a thrown navigation.
71
+ *
72
+ * Semantically only meaningful when the parent result is `type: 'error'`
73
+ * — `#launchBrowserAndScrape` sets it from inside its catch arm, and the
74
+ * success / skipped paths never write to it. The field is typed as
75
+ * optional on the whole envelope rather than narrowed to the error
76
+ * variant because beholder's `ScrapeResult` is not a discriminated
77
+ * union (all variants share the same shape and disambiguate via
78
+ * `type`), so narrowing here would force a parallel ad-hoc union with
79
+ * no compile-time payoff. Consumers MUST therefore check
80
+ * `result.type === 'error'` before reading `postNavigationUrl` — and in
81
+ * practice the only consumer is the JS-redirect rescue, which does
82
+ * exactly that.
83
+ *
84
+ * Consumers should also confirm the URL is meaningful via
85
+ * {@link deriveJsRedirectTarget} — `about:blank`, identity values,
86
+ * case-only or trailing-slash variants are all filtered there, not
87
+ * here.
88
+ */
89
+ postNavigationUrl?: string;
90
+ };
4
91
  /**
5
92
  * Configuration options that control crawler behavior.
6
93
  *
@@ -42,6 +129,89 @@ export interface CrawlerOptions extends Required<Pick<ParseURLOptions, 'disableQ
42
129
  userAgent: string;
43
130
  /** Whether to ignore robots.txt restrictions. */
44
131
  ignoreRobots: boolean;
132
+ /**
133
+ * Lookup for previously captured sub-resources, or `null` to disable the
134
+ * resource-reuse optimization. See {@link ResourceLookup}.
135
+ */
136
+ lookupResource: ResourceLookup | null;
137
+ /**
138
+ * Lookup for an already-persisted page's `source` column, or `null` when
139
+ * lineage propagation across sessions is not required. See
140
+ * {@link PageSourceLookup}.
141
+ *
142
+ * Injected by the orchestrator so that `#scrapePage` can resolve the
143
+ * parent's source on `--resume` / `--retry-failed` paths, where
144
+ * `inventoryMode` is not persisted but the page's `source` column is.
145
+ * Without this, sub-resources captured during a re-render of an
146
+ * inventory-labelled page would fall back to the DB DEFAULT `'crawled'`
147
+ * and lose their `'inventory-discovered'` provenance.
148
+ */
149
+ lookupPageSource: PageSourceLookup | null;
150
+ /**
151
+ * When non-null, the crawler is running in `--inventory` mode. New page
152
+ * rows whose URL matches `seedUrls` are labelled `'inventory-seed'`;
153
+ * every other newly-inserted page or sub-resource is labelled
154
+ * `'inventory-discovered'`. When `null`, no source label is emitted —
155
+ * the DB DEFAULT `'crawled'` applies.
156
+ */
157
+ inventoryMode: InventoryMode | null;
158
+ }
159
+ /**
160
+ * Inventory-mode runtime configuration. Passed from
161
+ * `CrawlerOrchestrator.inventory` into the Crawler so the emit pipeline can
162
+ * label new rows with the correct {@link PageSource}.
163
+ */
164
+ export interface InventoryMode {
165
+ /**
166
+ * URLs explicitly listed in the user-supplied URL file, keyed by their
167
+ * `withoutHashAndAuth` form (so credentials in the URL don't break the
168
+ * match). Membership decides `inventory-seed` vs `inventory-discovered`
169
+ * for HTML pages.
170
+ */
171
+ seedUrls: ReadonlySet<string>;
172
+ }
173
+ /**
174
+ * Looks up a previously captured sub-resource by URL.
175
+ *
176
+ * Injected by the orchestrator so that the crawler can reuse network data
177
+ * recorded during page rendering instead of issuing a redundant HEAD
178
+ * pre-flight request. Implementations must serialize the read against any
179
+ * pending resource writes (e.g., via the orchestrator's WriteQueue).
180
+ * @param urls - URL candidates to match (e.g., with and without auth credentials).
181
+ * @returns The recorded resource data, or `null` when no row matches.
182
+ */
183
+ export type ResourceLookup = (urls: readonly string[]) => Promise<ResourceLookupResult | null>;
184
+ /**
185
+ * Looks up the `source` column of a previously persisted page by URL.
186
+ *
187
+ * Returns `undefined` when no row matches (e.g. a freshly-discovered URL
188
+ * that has not been INSERTed yet) so the caller can fall through to its
189
+ * default behaviour.
190
+ *
191
+ * Used by {@link Crawler} during sub-resource lineage propagation:
192
+ * `#scrapePage` consults this once per page to resolve the parent's
193
+ * lineage when the in-memory `inventoryMode` is unavailable (i.e. on
194
+ * `--resume` / `--retry-failed` sessions where inventory state lives only
195
+ * in the DB).
196
+ * @param urlWithoutHashAndAuth - The URL key (`url.withoutHashAndAuth` form) to look up.
197
+ * @returns The recorded `source`, or `undefined` when no matching row exists.
198
+ */
199
+ export type PageSourceLookup = (urlWithoutHashAndAuth: string) => Promise<PageSource | undefined>;
200
+ /**
201
+ * Minimal sub-resource data needed to synthesize {@link PageData}
202
+ * without performing a network fetch.
203
+ */
204
+ export interface ResourceLookupResult {
205
+ /** HTTP status code of the recorded response, or `null` if unknown. */
206
+ status: number | null;
207
+ /** HTTP status text of the recorded response, or `null` if unknown. */
208
+ statusText: string | null;
209
+ /** The Content-Type header value (media type only), or `null` if unknown. */
210
+ contentType: string | null;
211
+ /** The Content-Length header value in bytes, or `null` if unknown. */
212
+ contentLength: number | null;
213
+ /** Raw HTTP response headers, or `null` if unavailable. */
214
+ responseHeaders: Record<string, string | string[] | undefined> | null;
45
215
  }
46
216
  /**
47
217
  * Describes a detected pagination pattern between two consecutive URLs.
@@ -67,6 +237,12 @@ export interface CrawlerEventTypes {
67
237
  page: {
68
238
  /** The scraped page data including HTML, metadata, anchors, and images. */
69
239
  result: PageData;
240
+ /**
241
+ * Inventory provenance to write to `pages.source` when this row is new.
242
+ * `undefined` means the DB default (`'crawled'`) applies, which is the
243
+ * common case outside `crawl --inventory`. See {@link PageSource}.
244
+ */
245
+ source?: PageSource;
70
246
  };
71
247
  /**
72
248
  * Emitted when an external page (outside the crawl scope) has been scraped.
@@ -74,6 +250,8 @@ export interface CrawlerEventTypes {
74
250
  externalPage: {
75
251
  /** The scraped page data for the external page. */
76
252
  result: PageData;
253
+ /** Inventory provenance for new rows — see {@link CrawlerEventTypes.page.source}. */
254
+ source?: PageSource;
77
255
  };
78
256
  /**
79
257
  * Emitted when a URL is skipped due to exclusion rules, robots.txt restrictions,
@@ -93,6 +271,14 @@ export interface CrawlerEventTypes {
93
271
  response: {
94
272
  /** The captured resource data including URL, status, content type, and headers. */
95
273
  resource: Resource;
274
+ /**
275
+ * Inventory provenance to write to `resources.source` when this row is new.
276
+ * Sub-resources discovered while puppeteer renders an inventory-seed
277
+ * page are always `'inventory-discovered'` (a sub-resource is never
278
+ * itself a seed). `undefined` means the DB default (`'crawled'`)
279
+ * applies. See {@link PageSource}.
280
+ */
281
+ source?: PageSource;
96
282
  };
97
283
  /**
98
284
  * Emitted to record the relationship between a page and a resource it references.
@@ -116,4 +302,46 @@ export interface CrawlerEventTypes {
116
302
  * (e.g., scrapeStart, headRequest, openPage, success).
117
303
  */
118
304
  changePhase: ChangePhaseEvent;
305
+ /**
306
+ * Emitted when a secondary scrape step fails for a URL but the page itself
307
+ * is otherwise scraped successfully (e.g. a viewport switch in
308
+ * `#fetchImages` detaches the frame and `@retryable` gives up). The
309
+ * orchestrator persists these as `page_errors` rows so the failure is
310
+ * visible in the archive instead of being lost to stdout logs.
311
+ *
312
+ * For ordering, this event is always emitted AFTER `page` / `externalPage`
313
+ * for the same URL, so the orchestrator's WriteQueue serialises the
314
+ * `pages` upsert before the `page_errors` insert and the FK resolution
315
+ * via URL succeeds.
316
+ */
317
+ pageError: {
318
+ /** URL of the affected page. */
319
+ url: string;
320
+ /** Scrape phase name (typically `'retryExhausted'`). */
321
+ phase: string;
322
+ /** Human-readable failure message. */
323
+ message: string;
324
+ /** Whether the URL is external to the crawl scope. */
325
+ isExternal: boolean;
326
+ };
327
+ /**
328
+ * Emitted when a URL redirects to a destination that has already been
329
+ * rendered during this crawl, so only the redirect edge is recorded and the
330
+ * destination is not re-rendered (#73). The orchestrator persists this via
331
+ * `Archive.setRedirect`, which writes the edge without overwriting the
332
+ * destination's content.
333
+ */
334
+ redirect: {
335
+ /** HEAD-resolved page data carrying the redirect chain (source → destination). */
336
+ result: PageData;
337
+ /**
338
+ * Inventory provenance for the redirect-edge call. Forwarded by the
339
+ * orchestrator to `Archive.setRedirect` → `Database.recordRedirect`
340
+ * so brand-new destination rows INSERTed by the edge-only path pick
341
+ * up the inventory label (and propagate it to intermediates) when
342
+ * the originating chain is in the inventory chain. `undefined` keeps
343
+ * the DB DEFAULT `'crawled'`.
344
+ */
345
+ source: PageSource | undefined;
346
+ };
119
347
  }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from './archive/types.js';
2
+ import type { InventoryMode } from './crawler/types.js';
2
3
  import type { CrawlEvent } from './types.js';
3
4
  import type { ExURL } from '@d-zero/shared/parse-url';
4
5
  import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
@@ -38,6 +39,12 @@ interface CrawlConfig extends Config {
38
39
  userAgent: string;
39
40
  /** Whether to ignore robots.txt restrictions. */
40
41
  ignoreRobots: boolean;
42
+ /**
43
+ * Inventory-mode runtime configuration (see {@link InventoryMode}). Set
44
+ * by {@link CrawlerOrchestrator.inventory}; the default crawl path leaves
45
+ * this `null` so new rows are labelled `'crawled'` by the DB DEFAULT.
46
+ */
47
+ inventoryMode: InventoryMode | null;
41
48
  }
42
49
  /**
43
50
  * Callback invoked after the CrawlerOrchestrator instance is fully initialized
@@ -82,11 +89,17 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
82
89
  * Sets up event listeners on the crawler, starts crawling, and resolves
83
90
  * when the crawl completes. Discovered pages, external pages, skipped pages,
84
91
  * and resources are forwarded to the archive for storage.
85
- * @param list - The list of parsed URLs to crawl. The first URL is used as the root.
92
+ * @param list - The list of parsed URLs to crawl. May be empty when a resumed
93
+ * session already has pending pages queued (for example `--retry-failed`).
94
+ * @param opts - Optional crawl overrides.
95
+ * @param opts.recursive - Whether discovered URLs are followed. Defaults to
96
+ * `!fromList` (recursive unless the archive was created from a URL list), so
97
+ * existing callers keep their behaviour; the retry flow passes it explicitly.
86
98
  * @returns A promise that resolves when crawling is complete.
87
- * @throws {Error} If the URL list is empty.
88
99
  */
89
- crawling(list: ExURL[]): Promise<void>;
100
+ crawling(list: ExURL[], opts?: {
101
+ recursive?: boolean;
102
+ }): Promise<void>;
90
103
  /**
91
104
  * Kill any zombie Chromium processes that were not properly cleaned up.
92
105
  *
@@ -146,6 +159,95 @@ export declare class CrawlerOrchestrator extends EventEmitter<CrawlEvent> {
146
159
  * @throws {Error} When `newUrls` is empty, the archive is in list mode, or it cannot be parsed.
147
160
  */
148
161
  static append(archivePath: string, newUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
162
+ /**
163
+ * Inventory mode: cross-reference a user-supplied URL list against an
164
+ * existing `.nitpicker` archive and import ONLY the URLs that are not yet
165
+ * tracked there. Designed to surface "orphan" landing pages that link
166
+ * graph traversal could not reach, and "unused" server-side files that
167
+ * no crawled page references — both of which the
168
+ * `listIsolatedPages` / `listUnusedResources` queries can then list.
169
+ *
170
+ * Flow:
171
+ *
172
+ * 1. Open the archive (writer mode, takes the archive lock).
173
+ * 2. Reject list-mode archives — they hold metadata-only rows that
174
+ * inventory has no business touching.
175
+ * 3. Reject archives with unfinished `pending` URLs — those would inherit
176
+ * the inventory `source` label by mistake. Operator must resume /
177
+ * retry-failed first.
178
+ * 4. Parse the URL list. Anything outside the archived scope is warned
179
+ * and skipped (inventory is per-server by design).
180
+ * 5. Subtract URLs that already exist in `pages` or `resources` so the
181
+ * second (and N-th) inventory pass is a no-op for known rows — keeps
182
+ * `'inventory-seed'` rows from being silently demoted.
183
+ * 6. Make `<archive>.bak`. Anything thrown beyond this point restores
184
+ * from the backup.
185
+ * 7. HEAD-probe each novel URL. Responses classified as HTML are queued
186
+ * as Crawler seeds (`'inventory-seed'`); everything else is recorded
187
+ * in `resources` directly as `'inventory-seed'` (no browser launch).
188
+ * 8. If any HTML seeds exist, start a Crawler with
189
+ * `inventoryMode = { seedUrls }` so the rendered page and every newly
190
+ * discovered downstream link is labelled correctly. `resume` is fed
191
+ * the existing `scraped` / `resources` sets so links into already-
192
+ * crawled pages stop at the seen-gate without re-rendering.
193
+ * 9. Drop the backup on success; restore it on any throw.
194
+ *
195
+ * Mutually exclusive with `--append` / `--retry-failed` / `--resume` /
196
+ * `--diff` / `--list` / `--list-file` / `--single` / `--output` — the
197
+ * CLI dispatch enforces this; this method assumes the caller honoured
198
+ * the contract.
199
+ * @param archivePath - Absolute or cwd-relative path to the `.nitpicker` archive.
200
+ * @param inventoryUrls - Pre-read URL list (one URL per element).
201
+ * @param options - Optional config overrides — most callers leave this blank and let the archived config flow through.
202
+ * @param initializedCallback - Hook invoked once the orchestrator is constructed but before `crawling` runs (the CLI uses it to attach progress reporting).
203
+ * @param sourceFileSha256 - **Pre-computed** SHA-256 hex digest of the
204
+ * source URL list. The orchestrator deliberately does NOT receive
205
+ * the file path: the path is privacy-sensitive (leaks user-home /
206
+ * OS structure when archives are shared) and we want it lifted off
207
+ * this boundary so no future log line / breadcrumb / error message
208
+ * inside the orchestrator can accidentally re-leak it. The CLI
209
+ * computes the digest via `computeFileSha256(resolvedListFile)`
210
+ * and passes it through here. Pass `null` for programmatic
211
+ * callers that built `inventoryUrls` in-memory; the audit row's
212
+ * `source_file_sha256` column will be `NULL`.
213
+ * @returns The orchestrator instance after a successful inventory pass.
214
+ * @throws {Error} When `inventoryUrls` is empty, the archive is in list mode, or pending URLs from a previous crawl remain unresolved.
215
+ */
216
+ static inventory(archivePath: string, inventoryUrls: string[], options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback, sourceFileSha256?: string | null): Promise<CrawlerOrchestrator>;
217
+ /**
218
+ * Re-fetch previously-failed pages in an existing `.nitpicker` archive.
219
+ *
220
+ * Opens the archive, resets every page whose previous attempt ended in a
221
+ * recoverable failure (missing status / content type, or a 5xx status — see
222
+ * {@link Archive.resetFailedPages}) back to pending, and resumes crawling.
223
+ * The archived crawl configuration is reused — scopes, excludes, keywords,
224
+ * user agent, etc. — so the retry honours the original crawl boundaries
225
+ * unless a field is explicitly overridden via `options`. The exception is
226
+ * `recursive`: it is taken from `options` (the CLI flag defaults it to
227
+ * `true`) rather than inherited from the archive, so a retry decides afresh
228
+ * whether to follow newly-discovered URLs regardless of how the original
229
+ * crawl was run.
230
+ *
231
+ * When `recursive` is enabled (the default), newly-discovered URLs from the
232
+ * re-fetched pages are followed and crawled from scratch; when disabled, only
233
+ * the failed pages themselves are re-fetched. The archived roots seed the
234
+ * crawl scope while the reset pages are picked up through the resumed pending
235
+ * set, so failed external pages stay external (metadata-only) instead of being
236
+ * promoted into scope, and a failed root is re-fetched in place.
237
+ *
238
+ * A `<archive>.bak` is created before any DB mutation and removed on success;
239
+ * if the crawl throws, the backup is restored to keep the original archive
240
+ * intact.
241
+ *
242
+ * List-mode archives (`info.fromList === true`) are rejected for the same
243
+ * reason as {@link CrawlerOrchestrator.append}: their pages are metadata-only.
244
+ * @param archivePath - Absolute or relative path to the existing `.nitpicker`.
245
+ * @param options - Optional config overrides applied on top of the archived config.
246
+ * @param initializedCallback - Optional callback invoked after initialization but before crawling resumes.
247
+ * @returns The orchestrator instance after the retry crawl completes.
248
+ * @throws {Error} When the archive is in list mode or has no parseable roots.
249
+ */
250
+ static retryFailed(archivePath: string, options?: Partial<CrawlConfig>, initializedCallback?: CrawlInitializedCallback): Promise<CrawlerOrchestrator>;
149
251
  /**
150
252
  * Resume a previously interrupted crawl from an existing archive file.
151
253
  *