@nitpicker/crawler 0.21.0 → 0.22.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 (113) hide show
  1. package/lib/append-retry-failed-common-setup-phases.d.ts +1 -1
  2. package/lib/append-retry-failed-common-setup-phases.js +5 -1
  3. package/lib/append-setup-phases.d.ts +1 -1
  4. package/lib/archive/archive.d.ts +57 -8
  5. package/lib/archive/archive.js +83 -12
  6. package/lib/archive/create-adjunct-tables.d.ts +3 -2
  7. package/lib/archive/create-adjunct-tables.js +12 -9
  8. package/lib/archive/create-entity-tables.js +3 -0
  9. package/lib/archive/database.d.ts +39 -7
  10. package/lib/archive/database.js +49 -9
  11. package/lib/archive/db-ops/_shared/keyset-paginate-by-id.d.ts +49 -0
  12. package/lib/archive/db-ops/_shared/keyset-paginate-by-id.js +69 -0
  13. package/lib/archive/db-ops/_shared/resolve-content-item-id.d.ts +13 -12
  14. package/lib/archive/db-ops/_shared/resolve-content-item-id.js +63 -31
  15. package/lib/archive/db-ops/_shared/types.d.ts +39 -0
  16. package/lib/archive/db-ops/analysis/replace-analysis-violations.js +7 -4
  17. package/lib/archive/db-ops/config/get-created-cwd.d.ts +11 -0
  18. package/lib/archive/db-ops/config/get-created-cwd.js +13 -0
  19. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  20. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.d.ts +57 -0
  21. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-observations.js +87 -0
  22. package/lib/archive/db-ops/errors/insert-page-error.js +3 -1
  23. package/lib/archive/db-ops/lifecycle/init.d.ts +13 -6
  24. package/lib/archive/db-ops/lifecycle/init.js +30 -10
  25. package/lib/archive/db-ops/list-reconcile/record-list-reconcile-run.d.ts +22 -0
  26. package/lib/archive/db-ops/{inventory/record-inventory-run.js → list-reconcile/record-list-reconcile-run.js} +11 -10
  27. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  28. package/lib/archive/db-ops/pages/read/get-crawling-state.d.ts +9 -2
  29. package/lib/archive/db-ops/pages/read/get-crawling-state.js +11 -3
  30. package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.d.ts +20 -0
  31. package/lib/archive/db-ops/pages/reset/clear-page-derived-rows.js +37 -0
  32. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +3 -2
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +17 -37
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +22 -1
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +79 -33
  36. package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.d.ts +78 -0
  37. package/lib/archive/db-ops/pages/reset/reset-pages-by-urls.js +146 -0
  38. package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.d.ts +24 -0
  39. package/lib/archive/db-ops/pages/reset/retryable-image-scan-codes.js +24 -0
  40. package/lib/archive/db-ops/pages/write/insert-inventory-content-items.js +2 -1
  41. package/lib/archive/db-ops/pages/write/insert-page.js +4 -2
  42. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +3 -1
  43. package/lib/archive/db-ops/pages/write/record-redirect.js +1 -1
  44. package/lib/archive/db-ops/pages/write/set-skipped-page.js +3 -1
  45. package/lib/archive/db-ops/pages/write/update-page.d.ts +9 -1
  46. package/lib/archive/db-ops/pages/write/update-page.js +28 -6
  47. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +3 -6
  48. package/lib/archive/db-ops/resources/get-resource-url-list.js +10 -35
  49. package/lib/archive/init-schema.d.ts +1 -1
  50. package/lib/archive/init-schema.js +3 -2
  51. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +11 -3
  52. package/lib/archive/meta/compute-main-contents-denormalized.js +14 -2
  53. package/lib/archive/meta/types.d.ts +24 -0
  54. package/lib/archive/migrate-content-items-is-metadata-only.d.ts +44 -0
  55. package/lib/archive/migrate-content-items-is-metadata-only.js +60 -0
  56. package/lib/archive/migrate-info-created-cwd.d.ts +17 -0
  57. package/lib/archive/migrate-info-created-cwd.js +36 -0
  58. package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.d.ts +34 -0
  59. package/lib/archive/migrate-inventory-runs-to-list-reconcile-runs.js +51 -0
  60. package/lib/archive/{migrate-inventory-runs-exclude-skipped.d.ts → migrate-list-reconcile-runs-exclude-skipped.d.ts} +8 -8
  61. package/lib/archive/{migrate-inventory-runs-exclude-skipped.js → migrate-list-reconcile-runs-exclude-skipped.js} +12 -12
  62. package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.d.ts +16 -0
  63. package/lib/archive/migrate-list-reconcile-runs-invalid-skipped.js +35 -0
  64. package/lib/archive/migrate-page-meta-image-scan.d.ts +27 -0
  65. package/lib/archive/migrate-page-meta-image-scan.js +52 -0
  66. package/lib/archive/page.d.ts +12 -0
  67. package/lib/archive/page.js +16 -0
  68. package/lib/archive/types.d.ts +97 -10
  69. package/lib/compute-auto-retry-backoff-delay.d.ts +30 -0
  70. package/lib/compute-auto-retry-backoff-delay.js +36 -0
  71. package/lib/crawler/apply-crawl-runtime-options-patch.d.ts +32 -0
  72. package/lib/crawler/apply-crawl-runtime-options-patch.js +129 -0
  73. package/lib/crawler/crawler.d.ts +35 -3
  74. package/lib/crawler/crawler.js +486 -338
  75. package/lib/crawler/dedupe/build-dedupe-cap-observation.d.ts +74 -0
  76. package/lib/crawler/dedupe/build-dedupe-cap-observation.js +97 -0
  77. package/lib/crawler/dedupe/compute-meta-signature.d.ts +7 -5
  78. package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
  79. package/lib/crawler/dedupe/compute-shape-key.d.ts +20 -0
  80. package/lib/crawler/dedupe/compute-shape-key.js +73 -1
  81. package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +6 -1
  82. package/lib/crawler/dedupe/dedupe-cap-tracker.js +6 -1
  83. package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +7 -5
  84. package/lib/crawler/dedupe/resolve-og-url-mismatch.js +5 -3
  85. package/lib/crawler/dedupe/types.d.ts +18 -0
  86. package/lib/crawler/fetch-destination.js +1 -0
  87. package/lib/crawler/link-list.d.ts +9 -1
  88. package/lib/crawler/link-list.js +12 -2
  89. package/lib/crawler/link-to-page-data.js +1 -0
  90. package/lib/crawler/resource-to-page-data.js +1 -0
  91. package/lib/crawler/types.d.ts +75 -0
  92. package/lib/crawler-orchestrator.d.ts +171 -8
  93. package/lib/crawler-orchestrator.js +1177 -234
  94. package/lib/crawler.d.ts +3 -0
  95. package/lib/crawler.js +3 -0
  96. package/lib/delay-or-abort.d.ts +20 -0
  97. package/lib/delay-or-abort.js +35 -0
  98. package/lib/pending-urls-remain-error.d.ts +35 -0
  99. package/lib/pending-urls-remain-error.js +43 -0
  100. package/lib/recrawl-setup-phases.d.ts +38 -0
  101. package/lib/recrawl-setup-phases.js +52 -0
  102. package/lib/resume-setup-phases.d.ts +1 -1
  103. package/lib/resume-setup-phases.js +5 -1
  104. package/lib/retry-failed-setup-phases.d.ts +1 -1
  105. package/lib/setup-recovery-phase-labels.d.ts +17 -8
  106. package/lib/setup-recovery-phase-labels.js +17 -8
  107. package/lib/types.d.ts +62 -12
  108. package/lib/utils/compute-file-sha256.d.ts +1 -1
  109. package/lib/utils/compute-file-sha256.js +1 -1
  110. package/package.json +4 -4
  111. package/lib/archive/db-ops/inventory/record-inventory-run.d.ts +0 -21
  112. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +0 -16
  113. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +0 -35
@@ -11,5 +11,5 @@
11
11
  */
12
12
  export declare const APPEND_RETRY_FAILED_COMMON_SETUP_PHASES: {
13
13
  readonly prefix: readonly ["Extracting archive", "Loading archive config", "Backing up archive"];
14
- readonly suffix: readonly ["Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
14
+ readonly suffix: readonly ["Loading dedupe-cap state", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
15
15
  };
@@ -12,7 +12,11 @@
12
12
  export const APPEND_RETRY_FAILED_COMMON_SETUP_PHASES = {
13
13
  prefix: ['Extracting archive', 'Loading archive config', 'Backing up archive'],
14
14
  suffix: [
15
- 'Loading dedupe-cap shape keys',
15
+ // Renamed from 'Loading dedupe-cap shape keys' — this phase now
16
+ // covers both the confirmed-capped shape-key read AND the
17
+ // not-yet-capped shape observation replay (`archive.listDedupeCapObservations`
18
+ // / `#preloadDedupeCapObservations`), not just the former.
19
+ 'Loading dedupe-cap state',
16
20
  'Loading crawl state',
17
21
  'Loading resource list',
18
22
  'Loading scraped page count',
@@ -14,4 +14,4 @@
14
14
  * a `SETUP_RECOVERY_PHASE_LABELS` entry (`'Restoring archive from backup'`)
15
15
  * is announced instead of continuing this sequence.
16
16
  */
17
- export declare const APPEND_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Repromoting external pages", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
17
+ export declare const APPEND_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Repromoting external pages", "Loading dedupe-cap state", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -1,5 +1,5 @@
1
1
  import type { TemplateClusterReason } from './db-ops/analysis/types.js';
2
- import type { Config, InsertDedupeCapEventParams, InsertNetworkOutageParams, InventoryRunMeta, PageSource } from './types.js';
2
+ import type { Config, DedupeCapObservationRow, InsertDedupeCapEventParams, InsertNetworkOutageParams, ListReconcileRunMeta, PageSource } from './types.js';
3
3
  import type { OutageWindow } from '../is-within-outage-window.js';
4
4
  import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
5
5
  import type { ConsoleLogEntry } from '@d-zero/beholder';
@@ -101,12 +101,19 @@ export default class Archive extends ArchiveAccessor {
101
101
  finalizeDedupeCapEvent(id: number, rejectedCount: number): Promise<void>;
102
102
  /**
103
103
  * Retrieves the current crawling state, including lists of scraped and pending URLs.
104
- * @returns An object with `scraped` and `pending` URL arrays.
104
+ * @returns An object with `scraped`, `pending`, and `pendingMetadataOnly` URL arrays.
105
105
  */
106
106
  getCrawlingState(): Promise<{
107
107
  scraped: string[];
108
108
  pending: string[];
109
+ pendingMetadataOnly: string[];
109
110
  }>;
111
+ /**
112
+ * Retrieves the `info.createdCwd` value stamped when this stub was
113
+ * created — see {@link Archive.resume} for how it is used.
114
+ * @returns The recorded cwd, or `null` if never stamped.
115
+ */
116
+ getCreatedCwd(): Promise<string | null>;
110
117
  /**
111
118
  * Return the subset of `urls` that already exist as `pages.url`. Used by
112
119
  * `CrawlerOrchestrator.inventory` to filter the user-supplied URL list
@@ -210,11 +217,26 @@ export default class Archive extends ArchiveAccessor {
210
217
  * Appends one open row to the `network_outages` journal.
211
218
  *
212
219
  * Thin facade over {@link Database.insertNetworkOutage} — see
213
- * {@link recordInventoryRun}'s docstring for why this indirection exists.
220
+ * {@link recordListReconcileRun}'s docstring for why this indirection exists.
214
221
  * @param params - The confirmed-outage fields to record.
215
222
  * @returns The autoincremented `id` of the inserted row.
216
223
  */
217
224
  insertNetworkOutage(params: InsertNetworkOutageParams): Promise<number>;
225
+ /**
226
+ * Every previously-scraped internal page's raw fields, in the shape
227
+ * `buildDedupeCapObservation` needs to replay this archive's prior
228
+ * `DedupeCapTracker` observations (not just its confirmed-capped
229
+ * shapes — see {@link listDedupeCapShapeKeys}) into a fresh tracker
230
+ * instance on `--resume` / `--append` / `--retry-failed` /
231
+ * `--inventory` / `--recrawl`. Without this, every not-yet-capped
232
+ * shape's Misra-Gries counter restarts at 0 each session, even one
233
+ * that was one observation away from confirming a trap.
234
+ * @param onProgress - Called after each internal chunk read, with the
235
+ * highest `content_items.id` scanned so far and the max id in the
236
+ * table. Omit for no reporting.
237
+ * @returns Every qualifying page's raw fields, in discovery order.
238
+ */
239
+ listDedupeCapObservations(onProgress?: (scannedUpToId: number, maxId: number) => void): Promise<DedupeCapObservationRow[]>;
218
240
  /**
219
241
  * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
220
242
  * Consumed by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s
@@ -246,16 +268,16 @@ export default class Archive extends ArchiveAccessor {
246
268
  */
247
269
  listNetworkOutages(): Promise<OutageWindow[]>;
248
270
  /**
249
- * Appends one row to the `inventory_runs` audit log.
271
+ * Appends one row to the `list_reconcile_runs` audit log.
250
272
  *
251
- * Thin facade over {@link Database.recordInventoryRun} — keeps the
273
+ * Thin facade over {@link Database.recordListReconcileRun} — keeps the
252
274
  * orchestrator decoupled from the knex layer and gives a single
253
275
  * write entry point that future Archive-level concerns (locking,
254
276
  * mirror sync, etc.) can hook into without touching every caller.
255
277
  * @param meta - The run metadata. Only `ran_at` is required.
256
278
  * @returns The autoincremented `id` of the inserted row.
257
279
  */
258
- recordInventoryRun(meta: InventoryRunMeta): Promise<number>;
280
+ recordListReconcileRun(meta: ListReconcileRunMeta): Promise<number>;
259
281
  /**
260
282
  * Releases the SQLite handle and the advisory lock **without** writing
261
283
  * the archive or removing `tmpDir`.
@@ -322,6 +344,19 @@ export default class Archive extends ArchiveAccessor {
322
344
  * @returns The URLs of the pages that were reset to pending.
323
345
  */
324
346
  resetFailedPages(onProgress?: (processed: number, total: number) => void): Promise<string[]>;
347
+ /**
348
+ * Reset pages matching an operator-supplied URL list back to pending so a
349
+ * follow-up crawl re-fetches them from scratch.
350
+ *
351
+ * Delegates to {@link Database.resetPagesByUrls}. See that method for the
352
+ * conservative exclusion rules (redirect sources / intentionally-skipped /
353
+ * external pages are matched but not reset).
354
+ * @param urls - URL strings to match, already in `withoutHashAndAuth` form.
355
+ * @param onProgress - Forwarded to {@link Database.resetPagesByUrls} — see
356
+ * that method's docs.
357
+ * @returns The reset URLs plus the excluded URLs grouped by reason.
358
+ */
359
+ resetPagesByUrls(urls: readonly string[], onProgress?: (processed: number, total: number) => void): Promise<import("./types.js").ResetPagesByUrlsResult>;
325
360
  /**
326
361
  * Persists the raw bytes of an `--inventory` source URL list into the
327
362
  * archive's tar payload, at `inventory/<sha256>.txt`.
@@ -368,8 +403,15 @@ export default class Archive extends ArchiveAccessor {
368
403
  * content-type), never a rendered body.
369
404
  * @param pageInfo - The page data to store.
370
405
  * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
406
+ * @param recursive - The crawl session's `recursive` option, forwarded so
407
+ * `is_metadata_only` on any newly-discovered anchor is computed
408
+ * correctly (#369). Defaults to `true` (never persist an anchor as
409
+ * metadata-only) since an external page's `anchorList` is always
410
+ * empty — this only matters for callers that pass one anyway.
411
+ * @example
412
+ * await archive.setExternalPage(externalPageData, undefined, options.recursive);
371
413
  */
372
- setExternalPage(pageInfo: PageData, source?: PageSource): Promise<void>;
414
+ setExternalPage(pageInfo: PageData, source?: PageSource, recursive?: boolean): Promise<void>;
373
415
  /**
374
416
  * Stores a crawled page's data in the archive database, persisting the
375
417
  * rendered HTML body as a zstd-compressed BLOB inside the same SQLite
@@ -380,9 +422,16 @@ export default class Archive extends ArchiveAccessor {
380
422
  * @param bodyHash - Precomputed body hash for the page's HTML (see
381
423
  * `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
382
424
  * computing it from the HTML instead.
425
+ * @param recursive - The crawl session's `recursive` option, forwarded so
426
+ * any anchor discovered on this page gets the correct
427
+ * `is_metadata_only` value (#369). Defaults to `true` (never persist
428
+ * an anchor as metadata-only) for callers that do not track the
429
+ * option.
383
430
  * @returns The database ID of the stored page.
431
+ * @example
432
+ * const pageId = await archive.setPage(pageData, undefined, undefined, options.recursive);
384
433
  */
385
- setPage(pageInfo: PageData, source?: PageSource, bodyHash?: Buffer | null): Promise<number>;
434
+ setPage(pageInfo: PageData, source?: PageSource, bodyHash?: Buffer | null, recursive?: boolean): Promise<number>;
386
435
  /**
387
436
  * Records a redirect edge without re-storing the destination's content.
388
437
  *
@@ -162,11 +162,19 @@ export default class Archive extends ArchiveAccessor {
162
162
  }
163
163
  /**
164
164
  * Retrieves the current crawling state, including lists of scraped and pending URLs.
165
- * @returns An object with `scraped` and `pending` URL arrays.
165
+ * @returns An object with `scraped`, `pending`, and `pendingMetadataOnly` URL arrays.
166
166
  */
167
167
  async getCrawlingState() {
168
168
  return this.#db.getCrawlingState();
169
169
  }
170
+ /**
171
+ * Retrieves the `info.createdCwd` value stamped when this stub was
172
+ * created — see {@link Archive.resume} for how it is used.
173
+ * @returns The recorded cwd, or `null` if never stamped.
174
+ */
175
+ async getCreatedCwd() {
176
+ return this.#db.getCreatedCwd();
177
+ }
170
178
  /**
171
179
  * Return the subset of `urls` that already exist as `pages.url`. Used by
172
180
  * `CrawlerOrchestrator.inventory` to filter the user-supplied URL list
@@ -303,7 +311,7 @@ export default class Archive extends ArchiveAccessor {
303
311
  * Appends one open row to the `network_outages` journal.
304
312
  *
305
313
  * Thin facade over {@link Database.insertNetworkOutage} — see
306
- * {@link recordInventoryRun}'s docstring for why this indirection exists.
314
+ * {@link recordListReconcileRun}'s docstring for why this indirection exists.
307
315
  * @param params - The confirmed-outage fields to record.
308
316
  * @returns The autoincremented `id` of the inserted row.
309
317
  */
@@ -311,6 +319,23 @@ export default class Archive extends ArchiveAccessor {
311
319
  dbLog('Insert network outage: startedAt=%d probeHost=%s', params.startedAt, params.probeHost);
312
320
  return await this.#db.insertNetworkOutage(params);
313
321
  }
322
+ /**
323
+ * Every previously-scraped internal page's raw fields, in the shape
324
+ * `buildDedupeCapObservation` needs to replay this archive's prior
325
+ * `DedupeCapTracker` observations (not just its confirmed-capped
326
+ * shapes — see {@link listDedupeCapShapeKeys}) into a fresh tracker
327
+ * instance on `--resume` / `--append` / `--retry-failed` /
328
+ * `--inventory` / `--recrawl`. Without this, every not-yet-capped
329
+ * shape's Misra-Gries counter restarts at 0 each session, even one
330
+ * that was one observation away from confirming a trap.
331
+ * @param onProgress - Called after each internal chunk read, with the
332
+ * highest `content_items.id` scanned so far and the max id in the
333
+ * table. Omit for no reporting.
334
+ * @returns Every qualifying page's raw fields, in discovery order.
335
+ */
336
+ async listDedupeCapObservations(onProgress) {
337
+ return this.#db.listDedupeCapObservations(onProgress);
338
+ }
314
339
  /**
315
340
  * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
316
341
  * Consumed by `CrawlerOrchestrator` to preload `DedupeCapTracker`'s
@@ -348,18 +373,18 @@ export default class Archive extends ArchiveAccessor {
348
373
  return await this.#db.listNetworkOutages();
349
374
  }
350
375
  /**
351
- * Appends one row to the `inventory_runs` audit log.
376
+ * Appends one row to the `list_reconcile_runs` audit log.
352
377
  *
353
- * Thin facade over {@link Database.recordInventoryRun} — keeps the
378
+ * Thin facade over {@link Database.recordListReconcileRun} — keeps the
354
379
  * orchestrator decoupled from the knex layer and gives a single
355
380
  * write entry point that future Archive-level concerns (locking,
356
381
  * mirror sync, etc.) can hook into without touching every caller.
357
382
  * @param meta - The run metadata. Only `ran_at` is required.
358
383
  * @returns The autoincremented `id` of the inserted row.
359
384
  */
360
- async recordInventoryRun(meta) {
361
- dbLog('Record inventory run: %s', meta.list_label ?? meta.ran_at);
362
- return await this.#db.recordInventoryRun(meta);
385
+ async recordListReconcileRun(meta) {
386
+ dbLog('Record list reconcile run: %s', meta.list_label ?? meta.ran_at);
387
+ return await this.#db.recordListReconcileRun(meta);
363
388
  }
364
389
  /**
365
390
  * Releases the SQLite handle and the advisory lock **without** writing
@@ -434,6 +459,22 @@ export default class Archive extends ArchiveAccessor {
434
459
  dbLog('Reset failed pages back to pending');
435
460
  return this.#db.resetFailedPages(onProgress);
436
461
  }
462
+ /**
463
+ * Reset pages matching an operator-supplied URL list back to pending so a
464
+ * follow-up crawl re-fetches them from scratch.
465
+ *
466
+ * Delegates to {@link Database.resetPagesByUrls}. See that method for the
467
+ * conservative exclusion rules (redirect sources / intentionally-skipped /
468
+ * external pages are matched but not reset).
469
+ * @param urls - URL strings to match, already in `withoutHashAndAuth` form.
470
+ * @param onProgress - Forwarded to {@link Database.resetPagesByUrls} — see
471
+ * that method's docs.
472
+ * @returns The reset URLs plus the excluded URLs grouped by reason.
473
+ */
474
+ async resetPagesByUrls(urls, onProgress) {
475
+ dbLog('Reset %d URL-list-matched page(s) back to pending', urls.length);
476
+ return this.#db.resetPagesByUrls(urls, onProgress);
477
+ }
437
478
  /**
438
479
  * Persists the raw bytes of an `--inventory` source URL list into the
439
480
  * archive's tar payload, at `inventory/<sha256>.txt`.
@@ -489,10 +530,17 @@ export default class Archive extends ArchiveAccessor {
489
530
  * content-type), never a rendered body.
490
531
  * @param pageInfo - The page data to store.
491
532
  * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
533
+ * @param recursive - The crawl session's `recursive` option, forwarded so
534
+ * `is_metadata_only` on any newly-discovered anchor is computed
535
+ * correctly (#369). Defaults to `true` (never persist an anchor as
536
+ * metadata-only) since an external page's `anchorList` is always
537
+ * empty — this only matters for callers that pass one anyway.
538
+ * @example
539
+ * await archive.setExternalPage(externalPageData, undefined, options.recursive);
492
540
  */
493
- async setExternalPage(pageInfo, source) {
541
+ async setExternalPage(pageInfo, source, recursive) {
494
542
  dbLog('Set external page: %s', pageInfo.url.href);
495
- await this.#db.updatePage(pageInfo, false, false, source);
543
+ await this.#db.updatePage(pageInfo, false, false, source, undefined, recursive);
496
544
  }
497
545
  /**
498
546
  * Stores a crawled page's data in the archive database, persisting the
@@ -504,11 +552,18 @@ export default class Archive extends ArchiveAccessor {
504
552
  * @param bodyHash - Precomputed body hash for the page's HTML (see
505
553
  * `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
506
554
  * computing it from the HTML instead.
555
+ * @param recursive - The crawl session's `recursive` option, forwarded so
556
+ * any anchor discovered on this page gets the correct
557
+ * `is_metadata_only` value (#369). Defaults to `true` (never persist
558
+ * an anchor as metadata-only) for callers that do not track the
559
+ * option.
507
560
  * @returns The database ID of the stored page.
561
+ * @example
562
+ * const pageId = await archive.setPage(pageData, undefined, undefined, options.recursive);
508
563
  */
509
- async setPage(pageInfo, source, bodyHash) {
564
+ async setPage(pageInfo, source, bodyHash, recursive) {
510
565
  dbLog('Set page: %s', pageInfo.url.href);
511
- return await this.#db.updatePage(pageInfo, true, pageInfo.isTarget, source, bodyHash);
566
+ return await this.#db.updatePage(pageInfo, true, pageInfo.isTarget, source, bodyHash, recursive);
512
567
  }
513
568
  /**
514
569
  * Records a redirect edge without re-storing the destination's content.
@@ -610,6 +665,15 @@ export default class Archive extends ArchiveAccessor {
610
665
  */
611
666
  async write(options) {
612
667
  saveLog('Starts: %s', this.#filePath);
668
+ // `.nitpicker` files are routinely shared between users — scrub the
669
+ // stub-local `createdCwd` (see `Config.createdCwd`'s JSDoc) before it
670
+ // gets folded into `db.sqlite` and tarred, so a packaged archive never
671
+ // carries another user's local absolute path. Bundled into the
672
+ // `checkpoint` step below (a single UPDATE ahead of the WAL fold-back)
673
+ // rather than its own `onStep` phase — both are near-instant and
674
+ // reporting them separately would add a phase label for no visible
675
+ // wait.
676
+ await this.#db.updateConfig({ createdCwd: null });
613
677
  options?.onStep?.('checkpoint');
614
678
  await this.#db.checkpoint();
615
679
  const filePathWithoutExt = path.resolve(path.dirname(this.#filePath), path.basename(this.#filePath, path.extname(this.#filePath)));
@@ -876,7 +940,14 @@ export default class Archive extends ArchiveAccessor {
876
940
  const db = await Archive.#connectDB(tmpDir, { onLog });
877
941
  const name = (await db.getName()) ||
878
942
  path.basename(targetPath).replace(Archive.TMP_DIR_PREFIX, '');
879
- const filePath = path.resolve(process.cwd(), name + '.' + Archive.FILE_EXTENSION);
943
+ // Reconstruct the output path from the cwd the interrupted
944
+ // session was originally started from (see `Config.createdCwd`),
945
+ // not this invocation's own `process.cwd()` — `crawl --resume
946
+ // <stub>` is routinely run from a different directory than the
947
+ // original `crawl`/`--append`/etc. call. Falls back to this
948
+ // invocation's cwd for a stub that predates this column.
949
+ const createdCwd = await db.getCreatedCwd();
950
+ const filePath = path.resolve(createdCwd ?? process.cwd(), name + '.' + Archive.FILE_EXTENSION);
880
951
  return new Archive(filePath, tmpDir, db, releaseLock);
881
952
  }
882
953
  catch (error) {
@@ -23,7 +23,8 @@ import type { Knex } from 'knex';
23
23
  * found in the main-content region, one row per DOM element, FK →
24
24
  * `content_items(id)`. Unlike its eight siblings above, captured by
25
25
  * nitpicker itself (`crawler/capture-custom-elements.ts`), not beholder
26
- * - `inventory_runs` — `--inventory` audit log (no FK; append-only)
26
+ * - `list_reconcile_runs` — `--inventory` / `--recrawl` audit log (no FK;
27
+ * append-only)
27
28
  * - `network_outages` — operator-network-outage journal (no FK; append-only
28
29
  * except `ended_at`, which is written once on recovery)
29
30
  * - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
@@ -60,7 +61,7 @@ import type { Knex } from 'knex';
60
61
  * Unlike `createRefTables` / `createEntityTables` (whose callers guard with
61
62
  * a single sentinel table), each table here is guarded individually because
62
63
  * the migration-script caller sees archives where any subset may already
63
- * exist (e.g. `page_jsonld` from the 0.10 migration but no `inventory_runs`).
64
+ * exist (e.g. `page_jsonld` from the 0.10 migration but no `list_reconcile_runs`).
64
65
  * Index creation stays inside each guard: an existing table keeps whatever
65
66
  * indexes its creation path declared.
66
67
  * @param instance - The Knex query builder instance connected to the database.
@@ -22,7 +22,8 @@
22
22
  * found in the main-content region, one row per DOM element, FK →
23
23
  * `content_items(id)`. Unlike its eight siblings above, captured by
24
24
  * nitpicker itself (`crawler/capture-custom-elements.ts`), not beholder
25
- * - `inventory_runs` — `--inventory` audit log (no FK; append-only)
25
+ * - `list_reconcile_runs` — `--inventory` / `--recrawl` audit log (no FK;
26
+ * append-only)
26
27
  * - `network_outages` — operator-network-outage journal (no FK; append-only
27
28
  * except `ended_at`, which is written once on recovery)
28
29
  * - `dedupe_cap_events` — `--dedupe-cap` same-cluster soft-cap audit log (no
@@ -59,7 +60,7 @@
59
60
  * Unlike `createRefTables` / `createEntityTables` (whose callers guard with
60
61
  * a single sentinel table), each table here is guarded individually because
61
62
  * the migration-script caller sees archives where any subset may already
62
- * exist (e.g. `page_jsonld` from the 0.10 migration but no `inventory_runs`).
63
+ * exist (e.g. `page_jsonld` from the 0.10 migration but no `list_reconcile_runs`).
63
64
  * Index creation stays inside each guard: an existing table keeps whatever
64
65
  * indexes its creation path declared.
65
66
  * @param instance - The Knex query builder instance connected to the database.
@@ -340,13 +341,15 @@ export async function createAdjunctTables(instance) {
340
341
  t.index('pageId');
341
342
  });
342
343
  }
343
- if (!(await instance.schema.hasTable('inventory_runs'))) {
344
- await instance.schema.createTable('inventory_runs', (t) => {
345
- // One row per successful `--inventory <list>` invocation. The
346
- // archive's audit log of "when did we apply which deploy list
347
- // at what scale". `.bak` is removed on success so this table
348
- // is the only durable provenance record. Column semantics live
349
- // on the `InventoryRunMeta` interface in `archive/types.ts`.
344
+ if (!(await instance.schema.hasTable('list_reconcile_runs'))) {
345
+ await instance.schema.createTable('list_reconcile_runs', (t) => {
346
+ // One row per successful `--inventory <list>` or `--recrawl
347
+ // <list>` invocation both reconcile a URL list against the
348
+ // archive via the same ingestion path. The archive's audit log
349
+ // of "when did we apply which list at what scale". `.bak` is
350
+ // removed on success so this table is the only durable
351
+ // provenance record. Column semantics live on the
352
+ // `ListReconcileRunMeta` interface in `archive/types.ts`.
350
353
  t.increments('id');
351
354
  t.string('ran_at').notNullable();
352
355
  t.string('list_label').nullable();
@@ -195,6 +195,7 @@ export async function createEntityTables(instance) {
195
195
  is_external INTEGER NOT NULL,
196
196
  scraped INTEGER NOT NULL,
197
197
  is_target INTEGER NOT NULL,
198
+ is_metadata_only INTEGER NOT NULL DEFAULT 0,
198
199
  status INTEGER,
199
200
  status_text TEXT,
200
201
  content_type_id INTEGER REFERENCES content_type_refs(id),
@@ -305,6 +306,8 @@ export async function createEntityTables(instance) {
305
306
  main_content_custom_element_count INTEGER,
306
307
  scroll_height_desktop INTEGER,
307
308
  scroll_height_mobile INTEGER,
309
+ image_scan_desktop INTEGER,
310
+ image_scan_mobile INTEGER,
308
311
  body_hash BLOB,
309
312
  console_error_count INTEGER
310
313
  )
@@ -1,6 +1,6 @@
1
1
  import type { TemplateClusterReason } from './db-ops/analysis/types.js';
2
2
  import type { JsonLdRow, MainContentAudioRow, MainContentButtonRow, MainContentCanvasRow, MainContentCustomElementRow, MainContentHeadingRow, MainContentIframeRow, MainContentImageRow, MainContentTableRow, MainContentVideoRow, PageTechnologyRow, TechnologySignalRow } from './meta/types.js';
3
- import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InsertDedupeCapEventParams, InsertNetworkOutageParams, InventoryRunMeta, PageFilter, PageSource } from './types.js';
3
+ import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, DedupeCapObservationRow, InsertDedupeCapEventParams, InsertNetworkOutageParams, ListReconcileRunMeta, PageFilter, PageSource, ResetPagesByUrlsResult } from './types.js';
4
4
  import type { OutageWindow } from '../is-within-outage-window.js';
5
5
  import type { PageData, Resource } from '../utils/types/types.js';
6
6
  import type { ConsoleLogEntry } from '@d-zero/beholder';
@@ -108,13 +108,22 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
108
108
  * Retrieves the current crawling state by listing scraped and pending URLs.
109
109
  * Delegates to {@link getCrawlingStateOp} — see the op for the strict
110
110
  * pending-set rationale.
111
- * @returns An object with `scraped` (completed URLs) and `pending` (the
112
- * strict set of in-scope, anchor-referenced, unfinished URLs).
111
+ * @returns An object with `scraped` (completed URLs), `pending` (the
112
+ * strict set of in-scope, anchor-referenced, unfinished URLs), and
113
+ * `pendingMetadataOnly` (the subset of `pending` fated for a
114
+ * metadata-only scrape — see the op's doc).
113
115
  */
114
116
  getCrawlingState(): Promise<{
115
117
  scraped: string[];
116
118
  pending: string[];
119
+ pendingMetadataOnly: string[];
117
120
  }>;
121
+ /**
122
+ * Retrieves the `info.createdCwd` value stamped at stub-creation time.
123
+ * Delegates to {@link getCreatedCwdOp}.
124
+ * @returns The recorded cwd, or `null` if never stamped.
125
+ */
126
+ getCreatedCwd(): Promise<string | null>;
118
127
  /**
119
128
  * Retrieves all `page_main_content_custom_elements` rows for the given
120
129
  * page id. Delegates to {@link getCustomElementsOfPageOp}.
@@ -370,6 +379,17 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
370
379
  * @param pageUrl - The URL of the page that references the resource.
371
380
  */
372
381
  insertResourceReferrers(src: string, pageUrl: string): Promise<void>;
382
+ /**
383
+ * Every previously-scraped internal page's raw fields, in the shape
384
+ * `buildDedupeCapObservation` needs to replay this archive's prior
385
+ * `DedupeCapTracker` observations into a fresh tracker instance.
386
+ * Delegates to {@link listDedupeCapObservationsOp} — see that function
387
+ * for the row selection criteria.
388
+ * @param onProgress - Forwarded to {@link listDedupeCapObservationsOp} —
389
+ * see that function's docs.
390
+ * @returns Every qualifying page's raw fields, in `content_items.id` order.
391
+ */
392
+ listDedupeCapObservations(onProgress?: (scannedUpToId: number, maxId: number) => void): Promise<DedupeCapObservationRow[]>;
373
393
  /**
374
394
  * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
375
395
  * Delegates to {@link listDedupeCapShapeKeysOp}.
@@ -392,12 +412,12 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
392
412
  */
393
413
  listNetworkOutages(): Promise<OutageWindow[]>;
394
414
  /**
395
- * Appends one row to the `inventory_runs` audit log.
396
- * Delegates to {@link recordInventoryRunOp}.
415
+ * Appends one row to the `list_reconcile_runs` audit log.
416
+ * Delegates to {@link recordListReconcileRunOp}.
397
417
  * @param meta - The run metadata to record. Only `ran_at` is required.
398
418
  * @returns The autoincremented `id` of the newly-inserted row.
399
419
  */
400
- recordInventoryRun(meta: InventoryRunMeta): Promise<number>;
420
+ recordListReconcileRun(meta: ListReconcileRunMeta): Promise<number>;
401
421
  /**
402
422
  * Records a redirect edge (source → destination) **without** re-storing the
403
423
  * destination's content. Delegates to {@link recordRedirectOp}.
@@ -461,6 +481,17 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
461
481
  * @returns The URLs of the pages that were reset to pending.
462
482
  */
463
483
  resetFailedPages(onProgress?: (processed: number, total: number) => void): Promise<string[]>;
484
+ /**
485
+ * Reset pages matching an operator-supplied URL list back to pending so a
486
+ * follow-up crawl re-fetches them from scratch. Delegates to
487
+ * {@link resetPagesByUrlsOp} — see the op for the conservative exclusion
488
+ * rationale (redirect sources / intentionally-skipped / external pages).
489
+ * @param urls - URL strings to match, already in `withoutHashAndAuth` form.
490
+ * @param onProgress - Forwarded to {@link resetPagesByUrlsOp} — see that
491
+ * function's docs.
492
+ * @returns The reset URLs plus the excluded URLs grouped by reason.
493
+ */
494
+ resetPagesByUrls(urls: readonly string[], onProgress?: (processed: number, total: number) => void): Promise<ResetPagesByUrlsResult>;
464
495
  /**
465
496
  * Stores the crawl configuration in the `info` table.
466
497
  * Delegates to {@link setConfigOp}.
@@ -503,9 +534,10 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
503
534
  * @param bodyHash - Precomputed body hash for the page's HTML (see
504
535
  * `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
505
536
  * computing it from the HTML instead.
537
+ * @param recursive - See {@link updatePageOp}'s `recursive` doc.
506
538
  * @returns The database `pageId` of the inserted/updated row.
507
539
  */
508
- updatePage(page: PageData, writeHtml: boolean, isTarget: boolean, source?: PageSource, bodyHash?: Buffer | null): Promise<number>;
540
+ updatePage(page: PageData, writeHtml: boolean, isTarget: boolean, source?: PageSource, bodyHash?: Buffer | null, recursive?: boolean): Promise<number>;
509
541
  /**
510
542
  * Creates and initializes a new Database instance.
511
543
  *
@@ -12,6 +12,7 @@ import { replacePageTemplates as replacePageTemplatesOp } from './db-ops/analysi
12
12
  import { getAnchorsOnPage as getAnchorsOnPageOp } from './db-ops/anchors/get-anchors-on-page.js';
13
13
  import { getBaseUrl as getBaseUrlOp } from './db-ops/config/get-base-url.js';
14
14
  import { getConfig as getConfigOp } from './db-ops/config/get-config.js';
15
+ import { getCreatedCwd as getCreatedCwdOp } from './db-ops/config/get-created-cwd.js';
15
16
  import { getName as getNameOp } from './db-ops/config/get-name.js';
16
17
  import { setConfig as setConfigOp } from './db-ops/config/set-config.js';
17
18
  import { updateConfig as updateConfigOp } from './db-ops/config/update-config.js';
@@ -19,15 +20,16 @@ import { replaceConsoleLogs as replaceConsoleLogsOp } from './db-ops/console-log
19
20
  import { accumulateDedupeCapRejectedCount as accumulateDedupeCapRejectedCountOp } from './db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js';
20
21
  import { finalizeDedupeCapEvent as finalizeDedupeCapEventOp } from './db-ops/dedupe-cap/finalize-dedupe-cap-event.js';
21
22
  import { insertDedupeCapEvent as insertDedupeCapEventOp } from './db-ops/dedupe-cap/insert-dedupe-cap-event.js';
23
+ import { listDedupeCapObservations as listDedupeCapObservationsOp } from './db-ops/dedupe-cap/list-dedupe-cap-observations.js';
22
24
  import { listDedupeCapShapeKeys as listDedupeCapShapeKeysOp } from './db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js';
23
25
  import { insertCrawlError as insertCrawlErrorOp } from './db-ops/errors/insert-crawl-error.js';
24
26
  import { insertPageError as insertPageErrorOp } from './db-ops/errors/insert-page-error.js';
25
27
  import { listDnsBurnedHostCandidates as listDnsBurnedHostCandidatesOp } from './db-ops/errors/list-dns-burned-host-candidates.js';
26
28
  import { getHtmlOfPageById as getHtmlOfPageByIdOp } from './db-ops/html/get-html-of-page-by-id.js';
27
- import { recordInventoryRun as recordInventoryRunOp } from './db-ops/inventory/record-inventory-run.js';
28
29
  import { checkpoint as checkpointOp } from './db-ops/lifecycle/checkpoint.js';
29
30
  import { destroy as destroyOp } from './db-ops/lifecycle/destroy.js';
30
31
  import { init as initOp } from './db-ops/lifecycle/init.js';
32
+ import { recordListReconcileRun as recordListReconcileRunOp } from './db-ops/list-reconcile/record-list-reconcile-run.js';
31
33
  import { getAudiosOfPage as getAudiosOfPageOp } from './db-ops/meta/get-audios-of-page.js';
32
34
  import { getButtonsOfPage as getButtonsOfPageOp } from './db-ops/meta/get-buttons-of-page.js';
33
35
  import { getCanvasesOfPage as getCanvasesOfPageOp } from './db-ops/meta/get-canvases-of-page.js';
@@ -53,6 +55,7 @@ import { getPages as getPagesOp } from './db-ops/pages/read/get-pages.js';
53
55
  import { getScrapedHtmlPageCount as getScrapedHtmlPageCountOp } from './db-ops/pages/read/get-scraped-html-page-count.js';
54
56
  import { repromoteExternalPages as repromoteExternalPagesOp } from './db-ops/pages/reset/repromote-external-pages.js';
55
57
  import { resetFailedPages as resetFailedPagesOp } from './db-ops/pages/reset/reset-failed-pages.js';
58
+ import { resetPagesByUrls as resetPagesByUrlsOp } from './db-ops/pages/reset/reset-pages-by-urls.js';
56
59
  import { insertInventorySeeds as insertInventorySeedsOp } from './db-ops/pages/write/insert-inventory-seeds.js';
57
60
  import { insertInventorySkippedPages as insertInventorySkippedPagesOp } from './db-ops/pages/write/insert-inventory-skipped-pages.js';
58
61
  import { recordRedirect as recordRedirectOp } from './db-ops/pages/write/record-redirect.js';
@@ -223,12 +226,22 @@ export class Database extends EventEmitter {
223
226
  * Retrieves the current crawling state by listing scraped and pending URLs.
224
227
  * Delegates to {@link getCrawlingStateOp} — see the op for the strict
225
228
  * pending-set rationale.
226
- * @returns An object with `scraped` (completed URLs) and `pending` (the
227
- * strict set of in-scope, anchor-referenced, unfinished URLs).
229
+ * @returns An object with `scraped` (completed URLs), `pending` (the
230
+ * strict set of in-scope, anchor-referenced, unfinished URLs), and
231
+ * `pendingMetadataOnly` (the subset of `pending` fated for a
232
+ * metadata-only scrape — see the op's doc).
228
233
  */
229
234
  async getCrawlingState() {
230
235
  return emitErrorAndRetry(this, 'Database.getCrawlingState', async () => await getCrawlingStateOp(this.#instance), retrySetting);
231
236
  }
237
+ /**
238
+ * Retrieves the `info.createdCwd` value stamped at stub-creation time.
239
+ * Delegates to {@link getCreatedCwdOp}.
240
+ * @returns The recorded cwd, or `null` if never stamped.
241
+ */
242
+ async getCreatedCwd() {
243
+ return emitErrorAndRetry(this, 'Database.getCreatedCwd', async () => await getCreatedCwdOp(this.#instance), retrySetting);
244
+ }
232
245
  /**
233
246
  * Retrieves all `page_main_content_custom_elements` rows for the given
234
247
  * page id. Delegates to {@link getCustomElementsOfPageOp}.
@@ -557,6 +570,19 @@ export class Database extends EventEmitter {
557
570
  async insertResourceReferrers(src, pageUrl) {
558
571
  return emitErrorAndRetry(this, 'Database.insertResourceReferrers', async () => await insertResourceReferrersOp(this.#instance, this.#writeRefCaches, src, pageUrl), retrySetting);
559
572
  }
573
+ /**
574
+ * Every previously-scraped internal page's raw fields, in the shape
575
+ * `buildDedupeCapObservation` needs to replay this archive's prior
576
+ * `DedupeCapTracker` observations into a fresh tracker instance.
577
+ * Delegates to {@link listDedupeCapObservationsOp} — see that function
578
+ * for the row selection criteria.
579
+ * @param onProgress - Forwarded to {@link listDedupeCapObservationsOp} —
580
+ * see that function's docs.
581
+ * @returns Every qualifying page's raw fields, in `content_items.id` order.
582
+ */
583
+ async listDedupeCapObservations(onProgress) {
584
+ return emitErrorAndRetry(this, 'Database.listDedupeCapObservations', async () => await listDedupeCapObservationsOp(this.#instance, onProgress), retrySetting);
585
+ }
560
586
  /**
561
587
  * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
562
588
  * Delegates to {@link listDedupeCapShapeKeysOp}.
@@ -585,13 +611,13 @@ export class Database extends EventEmitter {
585
611
  return emitErrorAndRetry(this, 'Database.listNetworkOutages', async () => await listNetworkOutagesOp(this.#instance), retrySetting);
586
612
  }
587
613
  /**
588
- * Appends one row to the `inventory_runs` audit log.
589
- * Delegates to {@link recordInventoryRunOp}.
614
+ * Appends one row to the `list_reconcile_runs` audit log.
615
+ * Delegates to {@link recordListReconcileRunOp}.
590
616
  * @param meta - The run metadata to record. Only `ran_at` is required.
591
617
  * @returns The autoincremented `id` of the newly-inserted row.
592
618
  */
593
- async recordInventoryRun(meta) {
594
- return emitErrorAndRetry(this, 'Database.recordInventoryRun', async () => await recordInventoryRunOp(this.#instance, meta), retrySetting);
619
+ async recordListReconcileRun(meta) {
620
+ return emitErrorAndRetry(this, 'Database.recordListReconcileRun', async () => await recordListReconcileRunOp(this.#instance, meta), retrySetting);
595
621
  }
596
622
  /**
597
623
  * Records a redirect edge (source → destination) **without** re-storing the
@@ -662,6 +688,19 @@ export class Database extends EventEmitter {
662
688
  async resetFailedPages(onProgress) {
663
689
  return emitErrorAndRetry(this, 'Database.resetFailedPages', async () => await resetFailedPagesOp(this.#instance, onProgress), retrySetting);
664
690
  }
691
+ /**
692
+ * Reset pages matching an operator-supplied URL list back to pending so a
693
+ * follow-up crawl re-fetches them from scratch. Delegates to
694
+ * {@link resetPagesByUrlsOp} — see the op for the conservative exclusion
695
+ * rationale (redirect sources / intentionally-skipped / external pages).
696
+ * @param urls - URL strings to match, already in `withoutHashAndAuth` form.
697
+ * @param onProgress - Forwarded to {@link resetPagesByUrlsOp} — see that
698
+ * function's docs.
699
+ * @returns The reset URLs plus the excluded URLs grouped by reason.
700
+ */
701
+ async resetPagesByUrls(urls, onProgress) {
702
+ return emitErrorAndRetry(this, 'Database.resetPagesByUrls', async () => await resetPagesByUrlsOp(this.#instance, urls, onProgress), retrySetting);
703
+ }
665
704
  /**
666
705
  * Stores the crawl configuration in the `info` table.
667
706
  * Delegates to {@link setConfigOp}.
@@ -712,10 +751,11 @@ export class Database extends EventEmitter {
712
751
  * @param bodyHash - Precomputed body hash for the page's HTML (see
713
752
  * `CrawlerEventTypes.page.bodyHash`). `undefined`/`null` falls back to
714
753
  * computing it from the HTML instead.
754
+ * @param recursive - See {@link updatePageOp}'s `recursive` doc.
715
755
  * @returns The database `pageId` of the inserted/updated row.
716
756
  */
717
- async updatePage(page, writeHtml, isTarget, source, bodyHash) {
718
- return emitErrorAndRetry(this, 'Database.updatePage', async () => await updatePageOp(this.#instance, this.#writeRefCaches, page, writeHtml, isTarget, source, bodyHash), retrySetting);
757
+ async updatePage(page, writeHtml, isTarget, source, bodyHash, recursive) {
758
+ return emitErrorAndRetry(this, 'Database.updatePage', async () => await updatePageOp(this.#instance, this.#writeRefCaches, page, writeHtml, isTarget, source, bodyHash, recursive), retrySetting);
719
759
  }
720
760
  /**
721
761
  * Initializes the database schema if tables do not exist, then runs