@nitpicker/crawler 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/lib/archive/archive.d.ts +136 -10
  2. package/lib/archive/archive.js +147 -9
  3. package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
  4. package/lib/archive/body-hash/compute-body-hash.js +31 -0
  5. package/lib/archive/body-hash/extract-body.d.ts +18 -0
  6. package/lib/archive/body-hash/extract-body.js +29 -0
  7. package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
  8. package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
  9. package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
  10. package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
  11. package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
  12. package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
  13. package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
  14. package/lib/archive/cache/clear-archive-cache-root.js +32 -0
  15. package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
  16. package/lib/archive/cache/list-archive-cache-entries.js +125 -0
  17. package/lib/archive/cache/path-exists.d.ts +9 -0
  18. package/lib/archive/cache/path-exists.js +18 -0
  19. package/lib/archive/cache/types.d.ts +33 -0
  20. package/lib/archive/cache/types.js +1 -0
  21. package/lib/archive/create-adjunct-tables.d.ts +14 -0
  22. package/lib/archive/create-adjunct-tables.js +169 -0
  23. package/lib/archive/create-entity-tables.js +21 -1
  24. package/lib/archive/database.d.ts +67 -2
  25. package/lib/archive/database.js +92 -3
  26. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
  27. package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
  28. package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
  29. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
  30. package/lib/archive/db-ops/_shared/types.d.ts +2 -0
  31. package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
  32. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +12 -3
  33. package/lib/archive/db-ops/analysis/replace-page-templates.js +35 -3
  34. package/lib/archive/db-ops/analysis/types.d.ts +61 -0
  35. package/lib/archive/db-ops/analysis/types.js +1 -0
  36. package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
  37. package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
  38. package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
  39. package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
  40. package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
  41. package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
  42. package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
  43. package/lib/archive/db-ops/console-logs/types.js +1 -0
  44. package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
  45. package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
  46. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.d.ts +18 -0
  47. package/lib/archive/db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js +23 -0
  48. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.d.ts +12 -0
  49. package/lib/archive/db-ops/dedupe-cap/finalize-dedupe-cap-event.js +15 -0
  50. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.d.ts +14 -0
  51. package/lib/archive/db-ops/dedupe-cap/insert-dedupe-cap-event.js +30 -0
  52. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.d.ts +21 -0
  53. package/lib/archive/db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js +27 -0
  54. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
  55. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
  56. package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
  57. package/lib/archive/db-ops/lifecycle/init.d.ts +11 -2
  58. package/lib/archive/db-ops/lifecycle/init.js +21 -2
  59. package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
  60. package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
  61. package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
  62. package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
  63. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
  64. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
  65. package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
  66. package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
  67. package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
  68. package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
  69. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +13 -0
  70. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +35 -7
  71. package/lib/archive/db-ops/pages/write/update-page.js +12 -0
  72. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
  73. package/lib/archive/filesystem/output-binary.d.ts +12 -0
  74. package/lib/archive/filesystem/output-binary.js +17 -0
  75. package/lib/archive/get-failed-page-messages.d.ts +20 -8
  76. package/lib/archive/get-failed-page-messages.js +29 -15
  77. package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
  78. package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
  79. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
  80. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
  81. package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
  82. package/lib/archive/migrate-page-meta-body-hash.js +52 -0
  83. package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
  84. package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
  85. package/lib/archive/page.d.ts +1 -1
  86. package/lib/archive/types.d.ts +60 -2
  87. package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
  88. package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
  89. package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
  90. package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
  91. package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
  92. package/lib/archive/url-alias/format-alias-key.js +14 -0
  93. package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
  94. package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
  95. package/lib/archive/url-alias/types.d.ts +7 -0
  96. package/lib/archive/url-alias/types.js +1 -0
  97. package/lib/classify-error-kind.d.ts +1 -0
  98. package/lib/classify-error-kind.js +14 -0
  99. package/lib/crawler/assert-chrome-installed.d.ts +24 -0
  100. package/lib/crawler/assert-chrome-installed.js +43 -0
  101. package/lib/crawler/choose-probe-host.d.ts +24 -0
  102. package/lib/crawler/choose-probe-host.js +38 -0
  103. package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
  104. package/lib/crawler/crawler.d.ts +12 -0
  105. package/lib/crawler/crawler.js +496 -29
  106. package/lib/crawler/decode-auth-credential.d.ts +29 -0
  107. package/lib/crawler/decode-auth-credential.js +39 -0
  108. package/lib/crawler/dedupe/compute-meta-signature.d.ts +30 -0
  109. package/lib/crawler/dedupe/compute-meta-signature.js +0 -0
  110. package/lib/crawler/dedupe/compute-shape-key.d.ts +37 -0
  111. package/lib/crawler/dedupe/compute-shape-key.js +56 -0
  112. package/lib/crawler/dedupe/dedupe-cap-tracker.d.ts +84 -0
  113. package/lib/crawler/dedupe/dedupe-cap-tracker.js +185 -0
  114. package/lib/crawler/dedupe/is-predicted-content-duplicate.d.ts +24 -0
  115. package/lib/crawler/dedupe/is-predicted-content-duplicate.js +26 -0
  116. package/lib/crawler/dedupe/is-shape-capped.d.ts +10 -0
  117. package/lib/crawler/dedupe/is-shape-capped.js +12 -0
  118. package/lib/crawler/dedupe/resolve-og-url-mismatch.d.ts +31 -0
  119. package/lib/crawler/dedupe/resolve-og-url-mismatch.js +40 -0
  120. package/lib/crawler/dedupe/types.d.ts +42 -0
  121. package/lib/crawler/dedupe/types.js +1 -0
  122. package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
  123. package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
  124. package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
  125. package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
  126. package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
  127. package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
  128. package/lib/crawler/fetch-destination.js +14 -2
  129. package/lib/crawler/generate-predicted-urls.d.ts +12 -0
  130. package/lib/crawler/generate-predicted-urls.js +33 -2
  131. package/lib/crawler/is-puppeteer-fallback-candidate.js +3 -0
  132. package/lib/crawler/network-gate.d.ts +49 -0
  133. package/lib/crawler/network-gate.js +78 -0
  134. package/lib/crawler/network-outage-detector.d.ts +51 -0
  135. package/lib/crawler/network-outage-detector.js +81 -0
  136. package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
  137. package/lib/crawler/network-outage-summary-counter.js +18 -0
  138. package/lib/crawler/probe-network.d.ts +26 -0
  139. package/lib/crawler/probe-network.js +21 -0
  140. package/lib/crawler/types.d.ts +185 -1
  141. package/lib/crawler-orchestrator.d.ts +74 -18
  142. package/lib/crawler-orchestrator.js +267 -27
  143. package/lib/crawler.d.ts +22 -0
  144. package/lib/crawler.js +18 -0
  145. package/lib/is-within-outage-window.d.ts +49 -0
  146. package/lib/is-within-outage-window.js +33 -0
  147. package/lib/network-related-error-kinds.d.ts +23 -0
  148. package/lib/network-related-error-kinds.js +28 -0
  149. package/lib/permanent-error-kinds.d.ts +9 -4
  150. package/lib/permanent-error-kinds.js +10 -4
  151. package/lib/types.d.ts +11 -2
  152. package/lib/utils/array/each-splitted.d.ts +1 -1
  153. package/lib/utils/compute-file-sha256.d.ts +17 -17
  154. package/lib/utils/compute-file-sha256.js +18 -47
  155. package/package.json +3 -3
@@ -1,6 +1,9 @@
1
+ import type { TemplateClusterReason } from './db-ops/analysis/types.js';
1
2
  import type { JsonLdRow, MainContentAudioRow, MainContentButtonRow, MainContentCanvasRow, MainContentHeadingRow, MainContentIframeRow, MainContentImageRow, MainContentTableRow, MainContentVideoRow, TagRow } from './meta/types.js';
2
- import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InventoryRunMeta, PageFilter, PageSource } from './types.js';
3
+ import type { Config, DatabaseOption, DB_Redirect, DB_Resource, DatabaseEvent, InsertDedupeCapEventParams, InsertNetworkOutageParams, InventoryRunMeta, PageFilter, PageSource } from './types.js';
4
+ import type { OutageWindow } from '../is-within-outage-window.js';
3
5
  import type { PageData, Resource } from '../utils/types/types.js';
6
+ import type { ConsoleLogEntry } from '@d-zero/beholder';
4
7
  import type { ExURL, ParseURLOptions } from '@d-zero/shared/parse-url';
5
8
  import type { Knex } from 'knex';
6
9
  import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
@@ -29,16 +32,39 @@ import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-awa
29
32
  export declare class Database extends EventEmitter<DatabaseEvent> {
30
33
  #private;
31
34
  private constructor();
35
+ /**
36
+ * Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
37
+ * looked up by `shape_key` rather than `id`. Delegates to
38
+ * {@link accumulateDedupeCapRejectedCountOp}.
39
+ * @param shapeKey - The capped shape whose rejection count to accumulate.
40
+ * @param rejectedCount - Additional anchors rejected for this shape in the current session.
41
+ */
42
+ accumulateDedupeCapRejectedCount(shapeKey: string, rejectedCount: number): Promise<void>;
32
43
  /**
33
44
  * Forces a WAL checkpoint, writing all pending WAL data back to the main
34
45
  * database file. Delegates to {@link checkpointOp}.
35
46
  */
36
47
  checkpoint(): Promise<void>;
48
+ /**
49
+ * Closes an outage row by stamping `ended_at` — a no-op if the row is
50
+ * already closed. Delegates to {@link closeNetworkOutageOp}.
51
+ * @param id - The `network_outages.id` to close.
52
+ * @param endedAt - Epoch ms the outage is considered to have ended.
53
+ */
54
+ closeNetworkOutage(id: number, endedAt: number): Promise<void>;
37
55
  /**
38
56
  * Destroys the database connection, releasing all pooled resources.
39
57
  * Delegates to {@link destroyOp}.
40
58
  */
41
59
  destroy(): Promise<void>;
60
+ /**
61
+ * Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
62
+ * no-op if the row is already finalized. Delegates to
63
+ * {@link finalizeDedupeCapEventOp}.
64
+ * @param id - The `dedupe_cap_events.id` to finalize.
65
+ * @param rejectedCount - Number of anchors rejected for this shape after it capped.
66
+ */
67
+ finalizeDedupeCapEvent(id: number, rejectedCount: number): Promise<void>;
42
68
  /**
43
69
  * Retrieves all anchors (outgoing links) on a specific page.
44
70
  * Delegates to {@link getAnchorsOnPageOp}.
@@ -263,6 +289,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
263
289
  * @param isExternal - Whether the URL is external to the crawl scope.
264
290
  */
265
291
  insertCrawlError(url: string | null, message: string, isExternal?: boolean): Promise<void>;
292
+ /**
293
+ * Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
294
+ * journal. Delegates to {@link insertDedupeCapEventOp}.
295
+ * @param params - The newly-capped shape's fields to record.
296
+ * @returns The autoincremented `id` of the newly-inserted row.
297
+ */
298
+ insertDedupeCapEvent(params: InsertDedupeCapEventParams): Promise<number>;
266
299
  /**
267
300
  * Pre-insert inventory non-HTML URLs into `resources` as placeholder rows.
268
301
  * Delegates to {@link insertInventoryResourcesOp}.
@@ -277,6 +310,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
277
310
  * @param urls - URL strings already in `withoutHashAndAuth` form.
278
311
  */
279
312
  insertInventorySeeds(urls: readonly string[]): Promise<void>;
313
+ /**
314
+ * Appends one open (`ended_at = NULL`) row to the `network_outages`
315
+ * journal. Delegates to {@link insertNetworkOutageOp}.
316
+ * @param params - The confirmed-outage fields to record.
317
+ * @returns The autoincremented `id` of the newly-inserted row.
318
+ */
319
+ insertNetworkOutage(params: InsertNetworkOutageParams): Promise<number>;
280
320
  /**
281
321
  * Records a partial scrape failure against the page identified by `url`.
282
322
  * Delegates to {@link insertPageErrorOp}.
@@ -300,6 +340,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
300
340
  * @param pageUrl - The URL of the page that references the resource.
301
341
  */
302
342
  insertResourceReferrers(src: string, pageUrl: string): Promise<void>;
343
+ /**
344
+ * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
345
+ * Delegates to {@link listDedupeCapShapeKeysOp}.
346
+ * @returns Distinct shape keys already confirmed capped, or `[]` on an
347
+ * archive that predates `dedupe_cap_events` or has recorded none.
348
+ */
349
+ listDedupeCapShapeKeys(): Promise<string[]>;
303
350
  /**
304
351
  * Hostnames whose `crawl_errors` history is consistently DNS failures and
305
352
  * for which no recent 2xx-3xx page or resource is recorded.
@@ -307,6 +354,13 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
307
354
  * @returns Lower-cased hostnames safe to short-circuit.
308
355
  */
309
356
  listDnsBurnedHostCandidates(): Promise<string[]>;
357
+ /**
358
+ * Lists every recorded outage as a resolved {@link OutageWindow}.
359
+ * Delegates to {@link listNetworkOutagesOp}.
360
+ * @returns Resolved outage windows, or `[]` on an archive that predates
361
+ * `network_outages` or has recorded no outages.
362
+ */
363
+ listNetworkOutages(): Promise<OutageWindow[]>;
310
364
  /**
311
365
  * Appends one row to the `inventory_runs` audit log.
312
366
  * Delegates to {@link recordInventoryRunOp}.
@@ -339,12 +393,23 @@ export declare class Database extends EventEmitter<DatabaseEvent> {
339
393
  line?: number | null;
340
394
  col?: number | null;
341
395
  }[]): Promise<void>;
396
+ /**
397
+ * Replaces one page's `page_console_logs` rows with a freshly captured
398
+ * set of console messages / page errors. Delegates to
399
+ * {@link replaceConsoleLogsOp}.
400
+ * @param pageUrl - The originally-requested URL, normalised (`withoutHashAndAuth` form).
401
+ * @param redirectPaths - The redirect chain hops captured during fetch, in order.
402
+ * @param entries - The console log entries to persist.
403
+ */
404
+ replaceConsoleLogs(pageUrl: string, redirectPaths: readonly string[], entries: readonly ConsoleLogEntry[]): Promise<void>;
342
405
  /**
343
406
  * Replaces the stored DOM-structure template classification with a
344
407
  * freshly generated set. Delegates to {@link replacePageTemplatesOp}.
345
408
  * @param templateKeysByUrl - Page URL → template key.
409
+ * @param clusterReasonsByTemplateKey - Template key → cluster-selection
410
+ * evidence, if the caller captured it.
346
411
  */
347
- replacePageTemplates(templateKeysByUrl: ReadonlyMap<string, string>): Promise<void>;
412
+ replacePageTemplates(templateKeysByUrl: ReadonlyMap<string, string>, clusterReasonsByTemplateKey?: ReadonlyMap<string, TemplateClusterReason>): Promise<void>;
348
413
  /**
349
414
  * Promote previously-external pages whose URL falls under any of the new
350
415
  * scope entries back to a "needs scraping" state.
@@ -15,6 +15,11 @@ import { getConfig as getConfigOp } from './db-ops/config/get-config.js';
15
15
  import { getName as getNameOp } from './db-ops/config/get-name.js';
16
16
  import { setConfig as setConfigOp } from './db-ops/config/set-config.js';
17
17
  import { updateConfig as updateConfigOp } from './db-ops/config/update-config.js';
18
+ import { replaceConsoleLogs as replaceConsoleLogsOp } from './db-ops/console-logs/replace-console-logs.js';
19
+ import { accumulateDedupeCapRejectedCount as accumulateDedupeCapRejectedCountOp } from './db-ops/dedupe-cap/accumulate-dedupe-cap-rejected-count.js';
20
+ import { finalizeDedupeCapEvent as finalizeDedupeCapEventOp } from './db-ops/dedupe-cap/finalize-dedupe-cap-event.js';
21
+ import { insertDedupeCapEvent as insertDedupeCapEventOp } from './db-ops/dedupe-cap/insert-dedupe-cap-event.js';
22
+ import { listDedupeCapShapeKeys as listDedupeCapShapeKeysOp } from './db-ops/dedupe-cap/list-dedupe-cap-shape-keys.js';
18
23
  import { insertCrawlError as insertCrawlErrorOp } from './db-ops/errors/insert-crawl-error.js';
19
24
  import { insertPageError as insertPageErrorOp } from './db-ops/errors/insert-page-error.js';
20
25
  import { listDnsBurnedHostCandidates as listDnsBurnedHostCandidatesOp } from './db-ops/errors/list-dns-burned-host-candidates.js';
@@ -33,6 +38,9 @@ import { getMainContentImagesOfPage as getMainContentImagesOfPageOp } from './db
33
38
  import { getMainContentTablesOfPage as getMainContentTablesOfPageOp } from './db-ops/meta/get-main-content-tables-of-page.js';
34
39
  import { getTagsOfPage as getTagsOfPageOp } from './db-ops/meta/get-tags-of-page.js';
35
40
  import { getVideosOfPage as getVideosOfPageOp } from './db-ops/meta/get-videos-of-page.js';
41
+ import { closeNetworkOutage as closeNetworkOutageOp } from './db-ops/outages/close-network-outage.js';
42
+ import { insertNetworkOutage as insertNetworkOutageOp } from './db-ops/outages/insert-network-outage.js';
43
+ import { listNetworkOutages as listNetworkOutagesOp } from './db-ops/outages/list-network-outages.js';
36
44
  import { setUrlOrder as setUrlOrderOp } from './db-ops/pages/order/set-url-order.js';
37
45
  import { getCrawlingState as getCrawlingStateOp } from './db-ops/pages/read/get-crawling-state.js';
38
46
  import { getExistingPageUrls as getExistingPageUrlsOp } from './db-ops/pages/read/get-existing-page-urls.js';
@@ -114,6 +122,16 @@ export class Database extends EventEmitter {
114
122
  },
115
123
  });
116
124
  }
125
+ /**
126
+ * Adds onto the `rejected_count` of a shape's `dedupe_cap_events` row,
127
+ * looked up by `shape_key` rather than `id`. Delegates to
128
+ * {@link accumulateDedupeCapRejectedCountOp}.
129
+ * @param shapeKey - The capped shape whose rejection count to accumulate.
130
+ * @param rejectedCount - Additional anchors rejected for this shape in the current session.
131
+ */
132
+ async accumulateDedupeCapRejectedCount(shapeKey, rejectedCount) {
133
+ return emitErrorAndRetry(this, 'Database.accumulateDedupeCapRejectedCount', async () => await accumulateDedupeCapRejectedCountOp(this.#instance, shapeKey, rejectedCount), retrySetting);
134
+ }
117
135
  /**
118
136
  * Forces a WAL checkpoint, writing all pending WAL data back to the main
119
137
  * database file. Delegates to {@link checkpointOp}.
@@ -121,6 +139,15 @@ export class Database extends EventEmitter {
121
139
  async checkpoint() {
122
140
  await checkpointOp(this.#instance);
123
141
  }
142
+ /**
143
+ * Closes an outage row by stamping `ended_at` — a no-op if the row is
144
+ * already closed. Delegates to {@link closeNetworkOutageOp}.
145
+ * @param id - The `network_outages.id` to close.
146
+ * @param endedAt - Epoch ms the outage is considered to have ended.
147
+ */
148
+ async closeNetworkOutage(id, endedAt) {
149
+ return emitErrorAndRetry(this, 'Database.closeNetworkOutage', async () => await closeNetworkOutageOp(this.#instance, id, endedAt), retrySetting);
150
+ }
124
151
  /**
125
152
  * Destroys the database connection, releasing all pooled resources.
126
153
  * Delegates to {@link destroyOp}.
@@ -128,6 +155,16 @@ export class Database extends EventEmitter {
128
155
  async destroy() {
129
156
  await destroyOp(this.#instance);
130
157
  }
158
+ /**
159
+ * Finalizes a `dedupe_cap_events` row by stamping `rejected_count` — a
160
+ * no-op if the row is already finalized. Delegates to
161
+ * {@link finalizeDedupeCapEventOp}.
162
+ * @param id - The `dedupe_cap_events.id` to finalize.
163
+ * @param rejectedCount - Number of anchors rejected for this shape after it capped.
164
+ */
165
+ async finalizeDedupeCapEvent(id, rejectedCount) {
166
+ return emitErrorAndRetry(this, 'Database.finalizeDedupeCapEvent', async () => await finalizeDedupeCapEventOp(this.#instance, id, rejectedCount), retrySetting);
167
+ }
131
168
  /**
132
169
  * Retrieves all anchors (outgoing links) on a specific page.
133
170
  * Delegates to {@link getAnchorsOnPageOp}.
@@ -409,6 +446,15 @@ export class Database extends EventEmitter {
409
446
  async insertCrawlError(url, message, isExternal = false) {
410
447
  return emitErrorAndRetry(this, 'Database.insertCrawlError', async () => await insertCrawlErrorOp(this.#instance, url, message, isExternal), retrySetting);
411
448
  }
449
+ /**
450
+ * Appends one row (`rejected_count = NULL`) to the `dedupe_cap_events`
451
+ * journal. Delegates to {@link insertDedupeCapEventOp}.
452
+ * @param params - The newly-capped shape's fields to record.
453
+ * @returns The autoincremented `id` of the newly-inserted row.
454
+ */
455
+ async insertDedupeCapEvent(params) {
456
+ return emitErrorAndRetry(this, 'Database.insertDedupeCapEvent', async () => await insertDedupeCapEventOp(this.#instance, params), retrySetting);
457
+ }
412
458
  /**
413
459
  * Pre-insert inventory non-HTML URLs into `resources` as placeholder rows.
414
460
  * Delegates to {@link insertInventoryResourcesOp}.
@@ -427,6 +473,15 @@ export class Database extends EventEmitter {
427
473
  async insertInventorySeeds(urls) {
428
474
  return emitErrorAndRetry(this, 'Database.insertInventorySeeds', async () => await insertInventorySeedsOp(this.#instance, this.#writeRefCaches, urls), retrySetting);
429
475
  }
476
+ /**
477
+ * Appends one open (`ended_at = NULL`) row to the `network_outages`
478
+ * journal. Delegates to {@link insertNetworkOutageOp}.
479
+ * @param params - The confirmed-outage fields to record.
480
+ * @returns The autoincremented `id` of the newly-inserted row.
481
+ */
482
+ async insertNetworkOutage(params) {
483
+ return emitErrorAndRetry(this, 'Database.insertNetworkOutage', async () => await insertNetworkOutageOp(this.#instance, params), retrySetting);
484
+ }
430
485
  /**
431
486
  * Records a partial scrape failure against the page identified by `url`.
432
487
  * Delegates to {@link insertPageErrorOp}.
@@ -456,6 +511,15 @@ export class Database extends EventEmitter {
456
511
  async insertResourceReferrers(src, pageUrl) {
457
512
  return emitErrorAndRetry(this, 'Database.insertResourceReferrers', async () => await insertResourceReferrersOp(this.#instance, this.#writeRefCaches, src, pageUrl), retrySetting);
458
513
  }
514
+ /**
515
+ * Every distinct `dedupe_cap_events.shape_key` recorded in this archive.
516
+ * Delegates to {@link listDedupeCapShapeKeysOp}.
517
+ * @returns Distinct shape keys already confirmed capped, or `[]` on an
518
+ * archive that predates `dedupe_cap_events` or has recorded none.
519
+ */
520
+ async listDedupeCapShapeKeys() {
521
+ return emitErrorAndRetry(this, 'Database.listDedupeCapShapeKeys', async () => await listDedupeCapShapeKeysOp(this.#instance), retrySetting);
522
+ }
459
523
  /**
460
524
  * Hostnames whose `crawl_errors` history is consistently DNS failures and
461
525
  * for which no recent 2xx-3xx page or resource is recorded.
@@ -465,6 +529,15 @@ export class Database extends EventEmitter {
465
529
  async listDnsBurnedHostCandidates() {
466
530
  return emitErrorAndRetry(this, 'Database.listDnsBurnedHostCandidates', async () => await listDnsBurnedHostCandidatesOp(this.#instance), retrySetting);
467
531
  }
532
+ /**
533
+ * Lists every recorded outage as a resolved {@link OutageWindow}.
534
+ * Delegates to {@link listNetworkOutagesOp}.
535
+ * @returns Resolved outage windows, or `[]` on an archive that predates
536
+ * `network_outages` or has recorded no outages.
537
+ */
538
+ async listNetworkOutages() {
539
+ return emitErrorAndRetry(this, 'Database.listNetworkOutages', async () => await listNetworkOutagesOp(this.#instance), retrySetting);
540
+ }
468
541
  /**
469
542
  * Appends one row to the `inventory_runs` audit log.
470
543
  * Delegates to {@link recordInventoryRunOp}.
@@ -494,13 +567,29 @@ export class Database extends EventEmitter {
494
567
  async replaceAnalysisViolations(violations) {
495
568
  return emitErrorAndRetry(this, 'Database.replaceAnalysisViolations', async () => await replaceAnalysisViolationsOp(this.#instance, violations), retrySetting);
496
569
  }
570
+ /**
571
+ * Replaces one page's `page_console_logs` rows with a freshly captured
572
+ * set of console messages / page errors. Delegates to
573
+ * {@link replaceConsoleLogsOp}.
574
+ * @param pageUrl - The originally-requested URL, normalised (`withoutHashAndAuth` form).
575
+ * @param redirectPaths - The redirect chain hops captured during fetch, in order.
576
+ * @param entries - The console log entries to persist.
577
+ */
578
+ async replaceConsoleLogs(pageUrl, redirectPaths, entries) {
579
+ return emitErrorAndRetry(this, 'Database.replaceConsoleLogs', async () => await replaceConsoleLogsOp(this.#instance, this.#writeRefCaches, pageUrl, redirectPaths, entries), retrySetting);
580
+ }
497
581
  /**
498
582
  * Replaces the stored DOM-structure template classification with a
499
583
  * freshly generated set. Delegates to {@link replacePageTemplatesOp}.
500
584
  * @param templateKeysByUrl - Page URL → template key.
501
- */
502
- async replacePageTemplates(templateKeysByUrl) {
503
- return emitErrorAndRetry(this, 'Database.replacePageTemplates', async () => await replacePageTemplatesOp(this.#instance, templateKeysByUrl), retrySetting);
585
+ * @param clusterReasonsByTemplateKey - Template key → cluster-selection
586
+ * evidence, if the caller captured it.
587
+ */
588
+ async replacePageTemplates(templateKeysByUrl, clusterReasonsByTemplateKey) {
589
+ return emitErrorAndRetry(this, 'Database.replacePageTemplates', async () => await replacePageTemplatesOp(this.#instance, {
590
+ templateKeysByUrl,
591
+ clusterReasonsByTemplateKey,
592
+ }), retrySetting);
504
593
  }
505
594
  /**
506
595
  * Promote previously-external pages whose URL falls under any of the new
@@ -30,5 +30,6 @@ export function clearWriteRefCaches(caches) {
30
30
  caches.contentTypeIds.clear();
31
31
  caches.jsonIds.clear();
32
32
  caches.blobIds.clear();
33
+ caches.consoleLogIds.clear();
33
34
  caches.headers = null;
34
35
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Compresses raw bytes for storage in a `codec`/`size_raw`/`size_stored`
3
+ * BLOB column, matching the read side of
4
+ * {@link import('./decode-json-ref.js').decodeJsonRef} and
5
+ * {@link import('../../decode-html-blob.js').decodeStoredBlob}.
6
+ *
7
+ * The one shared primitive behind every `codec: 'zstd'` writer in the
8
+ * archive (`upsert-json-ref.ts`, `write-page-html-blob.ts`,
9
+ * `replace-page-templates.ts`) — a codec change (e.g. a brotli fallback, a
10
+ * different zstd level) only has to happen here. Callers that also need a
11
+ * content hash (`upsert-json-ref.ts`, `write-page-html-blob.ts`) compute it
12
+ * themselves from the same `rawBytes` they pass in here, rather than this
13
+ * function computing one not every caller needs.
14
+ * @param rawBytes - The raw (uncompressed) payload bytes.
15
+ * @returns The compressed body alongside the codec and size bookkeeping the
16
+ * BLOB column's schema expects.
17
+ * @example
18
+ * const rawBytes = Buffer.from(JSON.stringify(reason), 'utf8');
19
+ * const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
20
+ */
21
+ export declare function compressPayload(rawBytes: Buffer): {
22
+ readonly body: Buffer;
23
+ readonly codec: 'zstd';
24
+ readonly sizeRaw: number;
25
+ readonly sizeStored: number;
26
+ };
@@ -0,0 +1,30 @@
1
+ import { zstdCompressSync } from 'node:zlib';
2
+ /**
3
+ * Compresses raw bytes for storage in a `codec`/`size_raw`/`size_stored`
4
+ * BLOB column, matching the read side of
5
+ * {@link import('./decode-json-ref.js').decodeJsonRef} and
6
+ * {@link import('../../decode-html-blob.js').decodeStoredBlob}.
7
+ *
8
+ * The one shared primitive behind every `codec: 'zstd'` writer in the
9
+ * archive (`upsert-json-ref.ts`, `write-page-html-blob.ts`,
10
+ * `replace-page-templates.ts`) — a codec change (e.g. a brotli fallback, a
11
+ * different zstd level) only has to happen here. Callers that also need a
12
+ * content hash (`upsert-json-ref.ts`, `write-page-html-blob.ts`) compute it
13
+ * themselves from the same `rawBytes` they pass in here, rather than this
14
+ * function computing one not every caller needs.
15
+ * @param rawBytes - The raw (uncompressed) payload bytes.
16
+ * @returns The compressed body alongside the codec and size bookkeeping the
17
+ * BLOB column's schema expects.
18
+ * @example
19
+ * const rawBytes = Buffer.from(JSON.stringify(reason), 'utf8');
20
+ * const { body, codec, sizeRaw, sizeStored } = compressPayload(rawBytes);
21
+ */
22
+ export function compressPayload(rawBytes) {
23
+ const compressed = zstdCompressSync(rawBytes);
24
+ return {
25
+ body: compressed,
26
+ codec: 'zstd',
27
+ sizeRaw: rawBytes.byteLength,
28
+ sizeStored: compressed.byteLength,
29
+ };
30
+ }
@@ -21,6 +21,7 @@ export function createWriteRefCaches() {
21
21
  contentTypeIds: new Map(),
22
22
  jsonIds: new Map(),
23
23
  blobIds: new Map(),
24
+ consoleLogIds: new Map(),
24
25
  headers: null,
25
26
  };
26
27
  }
@@ -42,6 +42,8 @@ export interface WriteRefCaches {
42
42
  readonly jsonIds: Map<string, number>;
43
43
  /** `hex(content hash) → blob_refs.id`. */
44
44
  readonly blobIds: Map<string, number>;
45
+ /** `hex(content hash) → console_log_items.id`. */
46
+ readonly consoleLogIds: Map<string, number>;
45
47
  /**
46
48
  * Header dictionary caches (`header_name_refs` / `header_value_refs` /
47
49
  * `header_sets` id maps). `null` until the first header-set upsert
@@ -1,5 +1,5 @@
1
- import { zstdCompressSync } from 'node:zlib';
2
1
  import { computeContentHash } from '../../populate-ref-tables/compute-content-hash.js';
2
+ import { compressPayload } from './compress-payload.js';
3
3
  /**
4
4
  * Resolves the `json_refs.id` for one JSON payload string, inserting the
5
5
  * deduplicated (hash-keyed) row when the payload is new.
@@ -27,11 +27,11 @@ export async function upsertJsonRef(qb, caches, jsonText) {
27
27
  if (cached !== undefined) {
28
28
  return cached;
29
29
  }
30
- const compressed = zstdCompressSync(rawBytes);
30
+ const { body, sizeRaw, sizeStored } = compressPayload(rawBytes);
31
31
  const rows = await qb.raw(`INSERT INTO json_refs (hash, json_text, codec, size_raw, size_stored)
32
32
  VALUES (?, ?, 'zstd', ?, ?)
33
33
  ON CONFLICT(hash) DO UPDATE SET hash = hash
34
- RETURNING id`, [hash, compressed, rawBytes.byteLength, compressed.byteLength]);
34
+ RETURNING id`, [hash, body, sizeRaw, sizeStored]);
35
35
  const first = rows[0];
36
36
  if (first === undefined) {
37
37
  throw new Error('upsertJsonRef: RETURNING yielded no row');
@@ -1,3 +1,4 @@
1
+ import type { ReplacePageTemplatesParams } from './types.js';
1
2
  import type { Knex } from 'knex';
2
3
  /**
3
4
  * Replaces the stored DOM-structure template classification (`--templates`)
@@ -13,8 +14,16 @@ import type { Knex } from 'knex';
13
14
  * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
14
15
  * and the stored `url_refs.url`) should not discard the rest of a
15
16
  * potentially multi-thousand-page classification run.
17
+ *
18
+ * `page_template_clusters` is always cleared alongside `page_templates`
19
+ * regardless of whether `clusterReasonsByTemplateKey` is passed — "no
20
+ * reason" must mean "not captured for this run", never "carry over the
21
+ * previous run's reason". Reason rows are inserted for every key in
22
+ * `clusterReasonsByTemplateKey` even if some have no surviving member page
23
+ * in `templateKeysByUrl` after URL-resolution skips above — harmless
24
+ * (nothing joins `page_template_clusters` back to `page_templates` by FK;
25
+ * see the table's own JSDoc), and simpler than cross-filtering the two maps.
16
26
  * @param knex - Knex query builder connected to the archive DB.
17
- * @param templateKeysByUrl - Page URL → template key, as produced by
18
- * `classifyPageTemplates`.
27
+ * @param params - See {@link ReplacePageTemplatesParams}.
19
28
  */
20
- export declare function replacePageTemplates(knex: Knex, templateKeysByUrl: ReadonlyMap<string, string>): Promise<void>;
29
+ export declare function replacePageTemplates(knex: Knex, params: ReplacePageTemplatesParams): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { eachSplitted } from '../../../utils/array/each-splitted.js';
2
+ import { compressPayload } from '../_shared/compress-payload.js';
2
3
  /**
3
4
  * Replaces the stored DOM-structure template classification (`--templates`)
4
5
  * with a freshly generated set.
@@ -13,13 +14,44 @@ import { eachSplitted } from '../../../utils/array/each-splitted.js';
13
14
  * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
14
15
  * and the stored `url_refs.url`) should not discard the rest of a
15
16
  * potentially multi-thousand-page classification run.
17
+ *
18
+ * `page_template_clusters` is always cleared alongside `page_templates`
19
+ * regardless of whether `clusterReasonsByTemplateKey` is passed — "no
20
+ * reason" must mean "not captured for this run", never "carry over the
21
+ * previous run's reason". Reason rows are inserted for every key in
22
+ * `clusterReasonsByTemplateKey` even if some have no surviving member page
23
+ * in `templateKeysByUrl` after URL-resolution skips above — harmless
24
+ * (nothing joins `page_template_clusters` back to `page_templates` by FK;
25
+ * see the table's own JSDoc), and simpler than cross-filtering the two maps.
16
26
  * @param knex - Knex query builder connected to the archive DB.
17
- * @param templateKeysByUrl - Page URL → template key, as produced by
18
- * `classifyPageTemplates`.
27
+ * @param params - See {@link ReplacePageTemplatesParams}.
19
28
  */
20
- export async function replacePageTemplates(knex, templateKeysByUrl) {
29
+ export async function replacePageTemplates(knex, params) {
30
+ const { templateKeysByUrl, clusterReasonsByTemplateKey } = params;
31
+ // Compressing every reason is pure CPU work independent of the DB — done
32
+ // before opening the transaction below so it doesn't extend how long the
33
+ // SQLite write-lock is held for.
34
+ const reasonRows = clusterReasonsByTemplateKey && clusterReasonsByTemplateKey.size > 0
35
+ ? [...clusterReasonsByTemplateKey].map(([templateKey, reason]) => {
36
+ const { body, codec, sizeRaw, sizeStored } = compressPayload(Buffer.from(JSON.stringify(reason), 'utf8'));
37
+ return {
38
+ template_key: templateKey,
39
+ member_count: reason.memberCount,
40
+ reason_json: body,
41
+ codec,
42
+ size_raw: sizeRaw,
43
+ size_stored: sizeStored,
44
+ };
45
+ })
46
+ : [];
21
47
  await knex.transaction(async (trx) => {
22
48
  await trx('page_templates').delete();
49
+ await trx('page_template_clusters').delete();
50
+ if (reasonRows.length > 0) {
51
+ await eachSplitted(reasonRows, 100, async (chunk) => {
52
+ await trx('page_template_clusters').insert(chunk);
53
+ });
54
+ }
23
55
  if (templateKeysByUrl.size === 0) {
24
56
  return;
25
57
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The six landmark types `@d-zero/page-cluster`'s `extractLandmarks` and
3
+ * `ClusterReason.landmarks` recognize.
4
+ */
5
+ export type TemplateClusterLandmarkType = 'header' | 'footer' | 'nav' | 'aside' | 'form' | 'search';
6
+ /**
7
+ * Why a Pass-0 block (one of possibly several that merged into a final
8
+ * cluster) was formed — mirrors `@d-zero/page-cluster`'s `BlockingReason`
9
+ * discriminated union.
10
+ */
11
+ export type TemplateClusterBlockingReason = {
12
+ readonly kind: 'css';
13
+ readonly distinctiveStylesheetHrefs: readonly string[];
14
+ } | {
15
+ readonly kind: 'path';
16
+ readonly pathKey: string;
17
+ } | {
18
+ readonly kind: 'orphanMerge';
19
+ readonly pathKey: string;
20
+ };
21
+ /** One block's blocking key and the reason it was formed. */
22
+ export interface TemplateClusterBlockingEvidence {
23
+ readonly blockKey: string;
24
+ readonly reason: TemplateClusterBlockingReason;
25
+ }
26
+ /** How common one landmark type is across a cluster's member pages. */
27
+ export interface TemplateClusterLandmarkProfile {
28
+ readonly presenceRate: number;
29
+ readonly chromeRate: number;
30
+ readonly shellTokens: readonly string[];
31
+ readonly memberCountWithInstance: number;
32
+ }
33
+ /**
34
+ * nitpicker's own copy of `@d-zero/page-cluster`'s `ClusterReason` shape.
35
+ * Kept independent of the `@d-zero/page-cluster` package (rather than
36
+ * importing its type directly) so `@nitpicker/query` — which does not
37
+ * depend on `@d-zero/page-cluster` and is consumed by the browser-side
38
+ * viewer build — never has to add that dependency just to type a value
39
+ * read back out of the archive. `@d-zero/page-cluster`'s `ClusterReason`
40
+ * is structurally assignable to this type.
41
+ */
42
+ export interface TemplateClusterReason {
43
+ readonly memberCount: number;
44
+ readonly blocking: readonly TemplateClusterBlockingEvidence[];
45
+ readonly structuralCoreTokens: readonly string[];
46
+ readonly landmarks: Partial<Record<TemplateClusterLandmarkType, TemplateClusterLandmarkProfile>>;
47
+ readonly siblingClusterKeys: readonly string[];
48
+ }
49
+ /**
50
+ * Params for {@link import('./replace-page-templates.js').replacePageTemplates}.
51
+ */
52
+ export interface ReplacePageTemplatesParams {
53
+ /** Page URL → template key, as produced by `classifyPageTemplates`. */
54
+ readonly templateKeysByUrl: ReadonlyMap<string, string>;
55
+ /**
56
+ * Template key → `@d-zero/page-cluster`'s cluster-selection evidence for
57
+ * that key, as produced by `classifyPageTemplates`. Omitted entirely (not
58
+ * just empty) when the caller didn't request reasons.
59
+ */
60
+ readonly clusterReasonsByTemplateKey?: ReadonlyMap<string, TemplateClusterReason>;
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { ConsoleLogContent } from './types.js';
2
+ /**
3
+ * Computes the `console_log_items.hash` for one console log entry's content.
4
+ *
5
+ * Hashing over the raw content fields (not their resolved ref ids) keeps the
6
+ * dictionary row's identity independent of ref-table state, matching
7
+ * `text_refs` / `json_refs` / `url_refs`, whose hash columns are likewise
8
+ * computed from source content rather than downstream ids. The same
9
+ * message logged by a shared framework on every page therefore hashes to
10
+ * the same value regardless of insertion order or which page's scrape
11
+ * resolves the dictionary row first.
12
+ * @param entry - The content fields to hash. `argsJson` must already be
13
+ * `stringifyConsoleLogArgs`'s output — this function does not
14
+ * re-serialize `args` itself.
15
+ * @returns 32-byte SHA-256 hash, ready for the `hash BLOB UNIQUE` column.
16
+ * @example
17
+ * const hash = computeConsoleLogHash({ type: 'error', text: 'boom', argsJson: null });
18
+ */
19
+ export declare function computeConsoleLogHash(entry: ConsoleLogContent): Buffer;
@@ -0,0 +1,30 @@
1
+ import { computeContentHash } from '../../populate-ref-tables/compute-content-hash.js';
2
+ /**
3
+ * Computes the `console_log_items.hash` for one console log entry's content.
4
+ *
5
+ * Hashing over the raw content fields (not their resolved ref ids) keeps the
6
+ * dictionary row's identity independent of ref-table state, matching
7
+ * `text_refs` / `json_refs` / `url_refs`, whose hash columns are likewise
8
+ * computed from source content rather than downstream ids. The same
9
+ * message logged by a shared framework on every page therefore hashes to
10
+ * the same value regardless of insertion order or which page's scrape
11
+ * resolves the dictionary row first.
12
+ * @param entry - The content fields to hash. `argsJson` must already be
13
+ * `stringifyConsoleLogArgs`'s output — this function does not
14
+ * re-serialize `args` itself.
15
+ * @returns 32-byte SHA-256 hash, ready for the `hash BLOB UNIQUE` column.
16
+ * @example
17
+ * const hash = computeConsoleLogHash({ type: 'error', text: 'boom', argsJson: null });
18
+ */
19
+ export function computeConsoleLogHash(entry) {
20
+ const canonical = JSON.stringify([
21
+ entry.type,
22
+ entry.text,
23
+ entry.argsJson,
24
+ entry.location?.url ?? null,
25
+ entry.location?.lineNumber ?? null,
26
+ entry.location?.columnNumber ?? null,
27
+ entry.stack ?? null,
28
+ ]);
29
+ return computeContentHash(canonical);
30
+ }
@@ -0,0 +1,34 @@
1
+ import type { WriteRefCaches } from '../_shared/types.js';
2
+ import type { ConsoleLogEntry } from '@d-zero/beholder';
3
+ import type { Knex } from 'knex';
4
+ /**
5
+ * Replaces one page's `page_console_logs` rows wholesale with a freshly
6
+ * captured set of console messages / page errors (issue #228).
7
+ *
8
+ * Scoped-Replace, the same pattern as `replaceAnchorEdges` /
9
+ * `replaceImageItems`: the page's existing rows are deleted and the new
10
+ * set is inserted in the same transaction, so a re-scrape (`--append` /
11
+ * `--retry-failed` / re-render) never accumulates duplicate occurrences.
12
+ * Unlike those two, there is no non-empty guard on the DELETE here — the
13
+ * caller (`Crawler#handleConsoleLogs`) already skips emitting the event
14
+ * entirely when `entries` is empty, which is what keeps a degraded
15
+ * re-scrape from wiping out a prior good capture.
16
+ *
17
+ * `pageUrl` is the originally-requested URL (matching `updatePage`'s
18
+ * `page.url.withoutHashAndAuth` contract), not necessarily the page that
19
+ * ends up holding the content: `resolveRedirectChain` derives the same
20
+ * redirect destination `updatePage` writes content under, so console logs
21
+ * attach to the row that actually carries the page's `page_meta`, not to
22
+ * an empty placeholder for the pre-redirect URL.
23
+ * @param knex - Knex query builder connected to the archive DB.
24
+ * @param caches - The connection's write-side id caches.
25
+ * @param pageUrl - The originally-requested URL, normalised
26
+ * (`withoutHashAndAuth` form).
27
+ * @param redirectPaths - The redirect chain hops captured during fetch, in
28
+ * order (empty when the page was not redirected).
29
+ * @param entries - The console log entries to persist. Must be non-empty —
30
+ * callers should skip calling this function entirely for an empty list.
31
+ * @example
32
+ * await replaceConsoleLogs(knex, caches, page.url.withoutHashAndAuth, page.redirectPaths, entries);
33
+ */
34
+ export declare function replaceConsoleLogs(knex: Knex, caches: WriteRefCaches, pageUrl: string, redirectPaths: readonly string[], entries: readonly ConsoleLogEntry[]): Promise<void>;