@nitpicker/crawler 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/lib/archive/archive-accessor.d.ts +87 -14
  2. package/lib/archive/archive-accessor.js +162 -36
  3. package/lib/archive/archive.d.ts +262 -24
  4. package/lib/archive/archive.js +397 -86
  5. package/lib/archive/cache/compute-archive-cache-key.d.ts +39 -0
  6. package/lib/archive/cache/compute-archive-cache-key.js +95 -0
  7. package/lib/archive/cache/extract-archive-to-cache.d.ts +43 -0
  8. package/lib/archive/cache/extract-archive-to-cache.js +309 -0
  9. package/lib/archive/cache/get-archive-cache-root.d.ts +20 -0
  10. package/lib/archive/cache/get-archive-cache-root.js +53 -0
  11. package/lib/archive/cache/is-archive-cache-disabled.d.ts +24 -0
  12. package/lib/archive/cache/is-archive-cache-disabled.js +34 -0
  13. package/lib/archive/cache/resolve-archive-cache-dir.d.ts +26 -0
  14. package/lib/archive/cache/resolve-archive-cache-dir.js +32 -0
  15. package/lib/archive/database.d.ts +411 -26
  16. package/lib/archive/database.js +1881 -540
  17. package/lib/archive/derive-lineage-from-parent.d.ts +37 -0
  18. package/lib/archive/derive-lineage-from-parent.js +42 -0
  19. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
  20. package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
  21. package/lib/archive/get-failed-page-messages.d.ts +43 -0
  22. package/lib/archive/get-failed-page-messages.js +131 -0
  23. package/lib/archive/init-schema.d.ts +53 -2
  24. package/lib/archive/init-schema.js +399 -15
  25. package/lib/archive/is-inventory-source.d.ts +21 -0
  26. package/lib/archive/is-inventory-source.js +22 -0
  27. package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
  28. package/lib/archive/meta/assert-compatible-version.js +72 -0
  29. package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
  30. package/lib/archive/meta/classify-jsonld-type.js +43 -0
  31. package/lib/archive/meta/compare-semver.d.ts +23 -0
  32. package/lib/archive/meta/compare-semver.js +51 -0
  33. package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
  34. package/lib/archive/meta/compute-page-denormalized.js +35 -0
  35. package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
  36. package/lib/archive/meta/derive-flat-from-meta.js +158 -0
  37. package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
  38. package/lib/archive/meta/derive-meta-extras.js +23 -0
  39. package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
  40. package/lib/archive/meta/extract-tags-for-archive.js +36 -0
  41. package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
  42. package/lib/archive/meta/summarize-jsonld.js +29 -0
  43. package/lib/archive/meta/summarize-tags.d.ts +16 -0
  44. package/lib/archive/meta/summarize-tags.js +33 -0
  45. package/lib/archive/meta/types.d.ts +207 -0
  46. package/lib/archive/meta/types.js +33 -0
  47. package/lib/archive/migrate-crawl-errors.d.ts +20 -0
  48. package/lib/archive/migrate-crawl-errors.js +38 -0
  49. package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
  50. package/lib/archive/migrate-html-blob-tables.js +53 -0
  51. package/lib/archive/migrate-inventory-runs.d.ts +29 -0
  52. package/lib/archive/migrate-inventory-runs.js +52 -0
  53. package/lib/archive/migrate-page-errors.d.ts +16 -0
  54. package/lib/archive/migrate-page-errors.js +35 -0
  55. package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
  56. package/lib/archive/migrate-pages-resources-source.js +46 -0
  57. package/lib/archive/page.d.ts +187 -49
  58. package/lib/archive/page.js +258 -63
  59. package/lib/archive/peek-archive-lock.d.ts +40 -0
  60. package/lib/archive/peek-archive-lock.js +62 -0
  61. package/lib/archive/resolve-redirect-chain.d.ts +33 -0
  62. package/lib/archive/resolve-redirect-chain.js +27 -0
  63. package/lib/archive/types.d.ts +168 -26
  64. package/lib/classify-error-kind.d.ts +19 -0
  65. package/lib/classify-error-kind.js +122 -0
  66. package/lib/crawler/build-js-redirect-edge.d.ts +68 -0
  67. package/lib/crawler/build-js-redirect-edge.js +57 -0
  68. package/lib/crawler/build-redirect-event.d.ts +24 -0
  69. package/lib/crawler/build-redirect-event.js +28 -0
  70. package/lib/crawler/clear-dns-burned-host-cache.d.ts +6 -0
  71. package/lib/crawler/clear-dns-burned-host-cache.js +11 -0
  72. package/lib/crawler/close-browser-safely.d.ts +64 -0
  73. package/lib/crawler/close-browser-safely.js +73 -0
  74. package/lib/crawler/crawler.d.ts +7 -2
  75. package/lib/crawler/crawler.js +919 -113
  76. package/lib/crawler/create-change-phase-handler.d.ts +54 -0
  77. package/lib/crawler/create-change-phase-handler.js +44 -0
  78. package/lib/crawler/derive-js-redirect-target.d.ts +68 -0
  79. package/lib/crawler/derive-js-redirect-target.js +129 -0
  80. package/lib/crawler/derive-page-source.d.ts +23 -0
  81. package/lib/crawler/derive-page-source.js +28 -0
  82. package/lib/crawler/derive-resource-source.d.ts +33 -0
  83. package/lib/crawler/derive-resource-source.js +37 -0
  84. package/lib/crawler/dns-burned-host-cache.d.ts +26 -0
  85. package/lib/crawler/dns-burned-host-cache.js +25 -0
  86. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +13 -0
  87. package/lib/crawler/dns-burned-host-short-circuit-counter.js +11 -0
  88. package/lib/crawler/drain-phase-errors.d.ts +48 -0
  89. package/lib/crawler/drain-phase-errors.js +35 -0
  90. package/lib/crawler/fetch-destination.d.ts +12 -4
  91. package/lib/crawler/fetch-destination.js +132 -18
  92. package/lib/crawler/format-crawl-progress.d.ts +12 -3
  93. package/lib/crawler/format-crawl-progress.js +14 -6
  94. package/lib/crawler/handle-browser-close.d.ts +29 -0
  95. package/lib/crawler/handle-browser-close.js +28 -0
  96. package/lib/crawler/is-html-content-type.d.ts +17 -0
  97. package/lib/crawler/is-html-content-type.js +19 -0
  98. package/lib/crawler/is-js-redirect-error-shape.d.ts +40 -0
  99. package/lib/crawler/is-js-redirect-error-shape.js +53 -0
  100. package/lib/crawler/is-likely-html-url.d.ts +22 -0
  101. package/lib/crawler/is-likely-html-url.js +65 -0
  102. package/lib/crawler/is-puppeteer-fallback-candidate.d.ts +16 -0
  103. package/lib/crawler/is-puppeteer-fallback-candidate.js +63 -0
  104. package/lib/crawler/kill-process-tree.d.ts +94 -0
  105. package/lib/crawler/kill-process-tree.js +178 -0
  106. package/lib/crawler/link-list.d.ts +21 -1
  107. package/lib/crawler/link-list.js +25 -4
  108. package/lib/crawler/link-to-page-data.d.ts +13 -5
  109. package/lib/crawler/link-to-page-data.js +26 -5
  110. package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
  111. package/lib/crawler/log-undrained-phase-errors.js +34 -0
  112. package/lib/crawler/normalize-content-type.d.ts +14 -0
  113. package/lib/crawler/normalize-content-type.js +20 -0
  114. package/lib/crawler/partition-urls-by-html.d.ts +16 -0
  115. package/lib/crawler/partition-urls-by-html.js +23 -0
  116. package/lib/crawler/plan-sub-resource-emits.d.ts +63 -0
  117. package/lib/crawler/plan-sub-resource-emits.js +44 -0
  118. package/lib/crawler/preload-short-circuit-error.d.ts +22 -0
  119. package/lib/crawler/preload-short-circuit-error.js +25 -0
  120. package/lib/crawler/redirect-dest-key.d.ts +19 -0
  121. package/lib/crawler/redirect-dest-key.js +27 -0
  122. package/lib/crawler/resource-to-page-data.d.ts +28 -0
  123. package/lib/crawler/resource-to-page-data.js +59 -0
  124. package/lib/crawler/should-burn-host.d.ts +78 -0
  125. package/lib/crawler/should-burn-host.js +61 -0
  126. package/lib/crawler/should-get-fallback-on-head-failure.d.ts +38 -0
  127. package/lib/crawler/should-get-fallback-on-head-failure.js +46 -0
  128. package/lib/crawler/types.d.ts +229 -1
  129. package/lib/crawler-orchestrator.d.ts +105 -3
  130. package/lib/crawler-orchestrator.js +620 -20
  131. package/lib/crawler.d.ts +8 -2
  132. package/lib/crawler.js +6 -1
  133. package/lib/permanent-error-kinds.d.ts +43 -0
  134. package/lib/permanent-error-kinds.js +48 -0
  135. package/lib/resource-row-to-lookup-result.d.ts +13 -0
  136. package/lib/resource-row-to-lookup-result.js +20 -0
  137. package/lib/types.d.ts +95 -1
  138. package/lib/utils/compute-file-sha256.d.ts +23 -0
  139. package/lib/utils/compute-file-sha256.js +55 -0
  140. package/lib/utils/error/emit-error-with-retry.d.ts +40 -0
  141. package/lib/utils/error/emit-error-with-retry.js +44 -0
  142. package/lib/utils/error/emit-error.d.ts +39 -0
  143. package/lib/utils/error/emit-error.js +41 -0
  144. package/lib/utils/object/parse-response-headers.d.ts +12 -0
  145. package/lib/utils/object/parse-response-headers.js +26 -0
  146. package/package.json +11 -11
  147. package/lib/utils/error/error-emitter.d.ts +0 -18
  148. package/lib/utils/error/error-emitter.js +0 -29
@@ -1,12 +1,15 @@
1
- import type { Config } from './types.js';
1
+ import type { Config, InventoryRunMeta, PageSource } from './types.js';
2
2
  import type { PageData, CrawlerError, Resource } from '../utils/types/types.js';
3
3
  import type { ExURL, ParseURLOptions } from '@d-zero/shared/parse-url';
4
4
  import { ArchiveAccessor } from './archive-accessor.js';
5
5
  /**
6
- * Main archive class for creating, opening, resuming, and writing Nitpicker archive files (`.nitpicker`).
6
+ * Main archive class for creating, opening, resuming, and writing Nitpicker
7
+ * archive files (`.nitpicker`).
7
8
  *
8
- * An Archive wraps a SQLite database and optional HTML snapshots into a compressed
9
- * tar archive. It extends {@link ArchiveAccessor} to provide read access to stored data.
9
+ * An Archive wraps a single SQLite database into a tar archive. HTML
10
+ * bodies live inside the same DB as zstd-compressed BLOBs (see #75) the
11
+ * tar payload is effectively just `db.sqlite`. It extends
12
+ * {@link ArchiveAccessor} to provide read access to stored data.
10
13
  *
11
14
  * Use the static factory methods ({@link Archive.create}, {@link Archive.open},
12
15
  * {@link Archive.resume}, {@link Archive.connect}) to obtain instances.
@@ -18,22 +21,42 @@ export default class Archive extends ArchiveAccessor {
18
21
  * The absolute file path of the archive (`.nitpicker` file).
19
22
  */
20
23
  get filePath(): string;
24
+ /**
25
+ * The intermediate directory `Archive.write()` produces by renaming
26
+ * `tmpDir` before tarring (`{cwd}/{archiveName}`). Exposed so the
27
+ * manager can include it in its cleanup-on-failure path: if `tar()`
28
+ * fails after the rename, this directory is orphaned and would
29
+ * otherwise be invisible to a `rmSync(tmpDir)` recovery.
30
+ */
31
+ get renamedDir(): string;
21
32
  private constructor();
22
33
  /**
23
34
  * @deprecated This method is no longer functional.
24
35
  */
25
36
  abort(): void;
26
37
  /**
27
- * Appends an error entry to the archive's error log file.
38
+ * Records a crawler-level error to both the human-readable `error.log` (full
39
+ * stack, for debugging) and the structured `crawl_errors` table (queryable,
40
+ * for the `error-kinds` analysis). The cause is not classified here — it is
41
+ * derived on read. `error.log` keeps the full stack while `crawl_errors`
42
+ * stores `error.message`; both normally carry the same cause token (e.g.
43
+ * `ENOTFOUND`), so classification agrees across the two — only an error whose
44
+ * cause lives solely in deeper stack frames could differ.
28
45
  * @param error - The crawler error object containing process and URL information.
29
46
  */
30
47
  addError(error: CrawlerError): Promise<void>;
31
48
  /**
32
- * Closes the archive. If the archive file does not yet exist on disk,
33
- * it writes the archive first. If the temporary directory still exists,
34
- * it is removed.
49
+ * Records a partial scrape failure against the page identified by `url`.
50
+ *
51
+ * The corresponding `pages` row is created on demand (or matched if it
52
+ * already exists), so the call works even if the page's normal data has
53
+ * not been written yet.
54
+ * @param url - URL of the affected page.
55
+ * @param phase - Scrape phase name (typically `'retryExhausted'`).
56
+ * @param message - Human-readable failure message.
57
+ * @param isExternal - Whether the URL is external. Defaults to `false`.
35
58
  */
36
- close(): Promise<void>;
59
+ addPageError(url: string, phase: string, message: string, isExternal?: boolean): Promise<void>;
37
60
  /**
38
61
  * Retrieves the current crawling state, including lists of scraped and pending URLs.
39
62
  * @returns An object with `scraped` and `pending` URL arrays.
@@ -42,11 +65,121 @@ export default class Archive extends ArchiveAccessor {
42
65
  scraped: string[];
43
66
  pending: string[];
44
67
  }>;
68
+ /**
69
+ * Return the subset of `urls` that already exist as `pages.url`. Used by
70
+ * `CrawlerOrchestrator.inventory` to filter the user-supplied URL list
71
+ * down to "URLs that are NOT yet in the archive" — only those reach the
72
+ * HEAD / scrape pipeline. Existing URLs are skipped to keep the second
73
+ * (and N-th) `--inventory` pass non-destructive.
74
+ * @param urls - Candidate URLs in `withoutHashAndAuth` form.
75
+ * @returns URLs already present in `pages`.
76
+ */
77
+ getExistingPageUrls(urls: readonly string[]): Promise<string[]>;
78
+ /**
79
+ * Return the subset of `urls` that already exist as `resources.url`. See
80
+ * {@link Archive.getExistingPageUrls} — the resource-side counterpart used
81
+ * by inventory mode to skip URLs that are already tracked as
82
+ * sub-resources.
83
+ * @param urls - Candidate URLs.
84
+ * @returns URLs already present in `resources`.
85
+ */
86
+ getExistingResourceUrls(urls: readonly string[]): Promise<string[]>;
87
+ /**
88
+ * Look up the `source` column of a single page row by its URL key. Thin
89
+ * facade over {@link Database.getPageSourceByUrl} — exposes the lookup
90
+ * to the orchestrator so it can inject a `PageSourceLookup` into the
91
+ * Crawler for sub-resource lineage propagation on `--resume` /
92
+ * `--retry-failed` sessions.
93
+ * @param url - URL key in `url.withoutHashAndAuth` form.
94
+ * @returns The recorded `source`, or `undefined` when no row exists.
95
+ */
96
+ getPageSourceByUrl(url: string): Promise<PageSource | undefined>;
97
+ /**
98
+ * Retrieves a single recorded sub-resource by its URL.
99
+ * @param urls - URL candidates to match against the stored resource URL.
100
+ * @returns The raw resource row, or `null` if none match.
101
+ */
102
+ getResourceByUrl(urls: readonly string[]): Promise<import("./types.js").DB_Resource | null>;
103
+ /**
104
+ * Counts the number of pages already scraped as crawl targets in the archive.
105
+ *
106
+ * Lets the crawler initialize its session-progress counter on resume so the
107
+ * displayed HTML-page count accounts for previously-rendered pages.
108
+ * @returns The count of pages with `isTarget = 1` and `scraped = 1`.
109
+ */
110
+ getScrapedHtmlPageCount(): Promise<number>;
45
111
  /**
46
112
  * Retrieves the base URL of the crawl session from the archive database.
47
113
  * @returns The base URL string.
48
114
  */
49
115
  getUrl(): Promise<any>;
116
+ /**
117
+ * Pre-insert inventory non-HTML URLs as `source='inventory-seed'`
118
+ * placeholders in the `resources` table — the non-HTML counterpart of
119
+ * {@link Archive.insertInventorySeeds}. Replaces the previous per-URL
120
+ * `setResources` loop in `CrawlerOrchestrator.inventory` so the
121
+ * ingestion phase commits all non-HTML rows in one chunked round-trip
122
+ * per 500 (a 50k-URL inventory list dropped from minutes-inside-`.bak`
123
+ * to seconds).
124
+ *
125
+ * Thin facade over {@link Database.insertInventoryResources}.
126
+ * `ExURL.href` is the storage key for `resources.url` (matches what
127
+ * `insertResource` writes for the per-URL path); we normalise here so
128
+ * the orchestrator stays decoupled from the storage form.
129
+ * @param urls - Non-HTML inventory URLs to record. No-op when empty.
130
+ */
131
+ insertInventoryResources(urls: readonly ExURL[]): Promise<void>;
132
+ /**
133
+ * Pre-insert inventory HTML seeds as `scraped=0`, `source='inventory-seed'`
134
+ * placeholder pages so the URL is durably tracked in the archive **before**
135
+ * the scrape phase starts. Thin facade over
136
+ * {@link Database.insertInventorySeeds} — see that method's JSDoc for the
137
+ * Ctrl+C-tolerance rationale and the `getCrawlingState` interaction.
138
+ *
139
+ * `ExURL` inputs are normalised to `withoutHashAndAuth` here so the storage
140
+ * key matches what `#getIdByUrl` writes for crawled rows, keeping the
141
+ * crawled-wins downgrade and the existing-URL filter (`getExistingPageUrls`)
142
+ * lookups consistent.
143
+ * @param urls - HTML seed URLs to pre-insert. No-op when empty.
144
+ */
145
+ insertInventorySeeds(urls: readonly ExURL[]): Promise<void>;
146
+ /**
147
+ * Hostnames whose `crawl_errors` history is consistently DNS failures and
148
+ * for which no recent 2xx/3xx page or resource is recorded. Consumed by
149
+ * `CrawlerOrchestrator.#preloadDnsBurnedHostCache` to seed the DNS-burned
150
+ * host cache at re-open (append / inventory / retryFailed / resume), so
151
+ * the next crawl skips HEAD pre-flight on hosts the previous crawl
152
+ * already proved unreachable.
153
+ *
154
+ * Deliberately exposed only on `Archive` (writer-side) — read-only
155
+ * `ArchiveAccessor` (stub viewer) does not see this method so the
156
+ * stub's no-migration contract is preserved.
157
+ * @returns Lower-cased hostnames safe to short-circuit.
158
+ */
159
+ listDnsBurnedHostCandidates(): Promise<string[]>;
160
+ /**
161
+ * Appends one row to the `inventory_runs` audit log.
162
+ *
163
+ * Thin facade over {@link Database.recordInventoryRun} — keeps the
164
+ * orchestrator decoupled from the knex layer and gives a single
165
+ * write entry point that future Archive-level concerns (locking,
166
+ * mirror sync, etc.) can hook into without touching every caller.
167
+ * @param meta - The run metadata. Only `ran_at` is required.
168
+ * @returns The autoincremented `id` of the inserted row.
169
+ */
170
+ recordInventoryRun(meta: InventoryRunMeta): Promise<number>;
171
+ /**
172
+ * Releases the SQLite handle and the advisory lock **without** writing
173
+ * the archive or removing `tmpDir`.
174
+ *
175
+ * Use this when you need to detach from a freshly-created `Archive`
176
+ * without finalising it — fixtures producing a stub state for tests,
177
+ * tooling that wants to leave the tmpDir alive for `crawl --resume`,
178
+ * or any non-orchestrator caller that owns the lifecycle externally.
179
+ * Shares the same idempotency guard as {@link close}, so the two paths
180
+ * are mutually exclusive (the first one called wins).
181
+ */
182
+ releaseHandle(): Promise<void>;
50
183
  /**
51
184
  * Promote previously-external pages that now fall under the (possibly extended)
52
185
  * scope back to a pending state so that the crawler re-scrapes them as fully
@@ -56,30 +189,58 @@ export default class Archive extends ArchiveAccessor {
56
189
  * @returns The URLs that were repromoted.
57
190
  */
58
191
  repromoteExternalPages(scopes: ReadonlyMap<string, readonly ExURL[]>, options?: ParseURLOptions): Promise<string[]>;
192
+ /**
193
+ * Reset previously-failed pages back to pending so a follow-up crawl re-fetches them.
194
+ *
195
+ * Delegates to {@link Database.resetFailedPages}. See that method for the
196
+ * exact failure criteria (missing status / content type, or a 5xx status).
197
+ * @returns The URLs of the pages that were reset to pending.
198
+ */
199
+ resetFailedPages(): Promise<string[]>;
59
200
  /**
60
201
  * Stores the crawl configuration into the archive database.
61
202
  * @param config - The configuration object to store.
62
203
  */
63
204
  setConfig(config: Config): Promise<number[]>;
64
205
  /**
65
- * Stores an external page's data in the archive database without saving a snapshot.
206
+ * Stores an external page's data in the archive database without storing
207
+ * an HTML snapshot. External-page rows carry only metadata (status, title,
208
+ * content-type), never a rendered body.
66
209
  * @param pageInfo - The page data to store.
210
+ * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
67
211
  */
68
- setExternalPage(pageInfo: PageData): Promise<void>;
212
+ setExternalPage(pageInfo: PageData, source?: PageSource): Promise<void>;
69
213
  /**
70
- * Stores a crawled page's data in the archive database and optionally saves an HTML snapshot.
71
- * If the snapshot file write fails, the HTML path in the database is cleared to prevent
72
- * referencing a non-existent file, and the error is re-thrown.
214
+ * Stores a crawled page's data in the archive database, persisting the
215
+ * rendered HTML body as a zstd-compressed BLOB inside the same SQLite
216
+ * transaction. Storage is content-addressable: identical bodies across
217
+ * pages share a single `page_html_blobs` row.
73
218
  * @param pageInfo - The page data to store.
219
+ * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
74
220
  * @returns The database ID of the stored page.
75
- * @throws {Error} Re-throws any error from the snapshot file write after clearing the HTML path.
76
221
  */
77
- setPage(pageInfo: PageData): Promise<number>;
222
+ setPage(pageInfo: PageData, source?: PageSource): Promise<number>;
223
+ /**
224
+ * Records a redirect edge without re-storing the destination's content.
225
+ *
226
+ * The crawler calls this (instead of {@link setPage}) when a URL redirects to
227
+ * a destination that has already been rendered (#73): only the source →
228
+ * destination edge is written, leaving the destination's stored title / meta /
229
+ * anchors / images untouched.
230
+ * @param pageInfo - The HEAD-resolved page data carrying the redirect chain.
231
+ * @param source - Inventory provenance for a brand-new destination row.
232
+ * Forwarded to `recordRedirect` so the destination's `source` (and the
233
+ * chain-intermediate `source` derived from it) lands on the inventory
234
+ * label instead of the DB DEFAULT `'crawled'` when the orchestrator is
235
+ * running an inventory pass. `undefined` keeps the DB DEFAULT.
236
+ */
237
+ setRedirect(pageInfo: PageData, source?: PageSource): Promise<void>;
78
238
  /**
79
239
  * Stores a sub-resource (CSS, JS, image, etc.) in the archive database.
80
240
  * @param resource - The resource data to store.
241
+ * @param source - Provenance label for new rows. `undefined` leaves the DB DEFAULT (`'crawled'`).
81
242
  */
82
- setResources(resource: Resource): Promise<void>;
243
+ setResources(resource: Resource, source?: PageSource): Promise<void>;
83
244
  /**
84
245
  * Stores the referrer relationship between a resource and the page that references it.
85
246
  * @param params - An object containing `url` (the page URL) and `src` (the resource URL).
@@ -109,30 +270,85 @@ export default class Archive extends ArchiveAccessor {
109
270
  */
110
271
  updateConfig(patch: Partial<Config>): Promise<void>;
111
272
  /**
112
- * Writes the archive to disk as a compressed `.nitpicker` file.
273
+ * Writes the archive to disk as a `.nitpicker` tar file.
113
274
  *
114
- * This method compresses the HTML snapshot directory into a zip file,
115
- * renames the temporary working directory, and creates the final tar archive.
116
- * The temporary directory is removed after writing.
275
+ * Checkpoints the SQLite WAL so the database is self-contained inside
276
+ * `db.sqlite`, renames the temporary working directory to the archive's
277
+ * basename, and tars it into the final `.nitpicker`. The tar container
278
+ * holds a single `db.sqlite` file (the legacy `snapshot-html.zip` is gone
279
+ * — HTML lives as BLOBs in the DB), so finalisation is effectively a
280
+ * single-file copy with no per-snapshot syscalls.
117
281
  */
118
282
  write(): Promise<void>;
119
283
  /** The file extension for Nitpicker archive files (without the leading dot). */
120
284
  static FILE_EXTENSION: string;
121
- /** The directory name used for storing HTML snapshots within the archive. */
122
- static readonly SNAPSHOT_HTML_DIR = "snapshot-html";
123
285
  /** The filename of the SQLite database within the archive. */
124
286
  static readonly SQLITE_DB_FILE_NAME = "db.sqlite";
125
287
  /** The prefix used for temporary working directories during archive operations. */
126
288
  static TMP_DIR_PREFIX: string;
127
289
  /**
128
290
  * Opens a read-only connection to an existing archive's database.
291
+ *
129
292
  * Returns an {@link ArchiveAccessor} that provides query methods
130
- * without the ability to modify or write the archive.
293
+ * without the ability to modify or write the archive. The DB is opened
294
+ * in **read-only mode**: no schema migrations run, and the connection
295
+ * refuses to resurrect a missing parent directory or db file (so a
296
+ * TOCTOU window between source classification and this call cannot
297
+ * silently produce an empty phantom tmpDir).
298
+ *
299
+ * The returned accessor is also marked read-only so consumer-facing
300
+ * helpers (e.g. {@link ArchiveAccessor.getHtmlOfPage}) avoid any
301
+ * filesystem mutation on the user's tmpDir.
131
302
  * @param tmpDir - The path to the temporary directory containing the database.
132
303
  * @param namespace - An optional namespace for scoping data access within the archive.
133
304
  * @returns An ArchiveAccessor instance for querying the archive data.
134
305
  */
135
306
  static connect(tmpDir: string, namespace?: string | null): Promise<ArchiveAccessor>;
307
+ /**
308
+ * Open a `.nitpicker` archive through the read-only tar cache.
309
+ *
310
+ * This is the fast path for read-only consumers (viewer, MCP, query
311
+ * CLI). It diverges from {@link Archive.open} in two important ways:
312
+ *
313
+ * 1. The extracted contents land in an OS-temp-scoped cache directory
314
+ * keyed by the archive's `size + mtime_ns + ctime_ns` (see
315
+ * {@link computeArchiveCacheKey}). Subsequent opens of the same
316
+ * unchanged archive skip the untar entirely. A fresh 10 GB archive
317
+ * pays the ~10 s untar cost once; reopens are instant.
318
+ * 2. The returned value is an {@link ArchiveAccessor} (read-only), not
319
+ * an `Archive` (writer). Closing it tears down the DB handle but
320
+ * leaves the cache directory in place for the next reader. The
321
+ * OS's own temp-directory cleanup (macOS reboot, Linux
322
+ * `systemd-tmpfiles`, Windows Disk Cleanup) reclaims stale
323
+ * entries — we do not own eviction.
324
+ *
325
+ * Migrations: the writer-side migration stack
326
+ * (`initSchema` / `migrate*`) runs once at cache-miss extraction, so
327
+ * the cache directory always lands on the current schema before the
328
+ * read-only re-open. Cache hits then skip migrations entirely.
329
+ *
330
+ * Override the cache location with `NITPICKER_TAR_CACHE_DIR`. The
331
+ * disable switch (`NITPICKER_DISABLE_TAR_CACHE=1`) is honoured by
332
+ * the caller (`ArchiveManager.open` falls back to {@link Archive.open}
333
+ * in that case); this function itself always goes through the cache.
334
+ *
335
+ * Writer entry points (`crawl --append`, `crawl --retry-failed`) must
336
+ * NOT use this path — they need the lock + write-back semantics of
337
+ * {@link Archive.open}.
338
+ * @param filePath - Absolute path to the `.nitpicker` file.
339
+ * @param namespace - Optional namespace forwarded to {@link ArchiveAccessor}.
340
+ * @returns A read-only {@link ArchiveAccessor} backed by the cache directory.
341
+ * @example
342
+ * ```ts
343
+ * const accessor = await Archive.openCached('/path/to/site.nitpicker');
344
+ * try {
345
+ * const summary = await getSummary(accessor);
346
+ * } finally {
347
+ * await accessor.close(); // tears down DB handle, cacheDir persists.
348
+ * }
349
+ * ```
350
+ */
351
+ static openCached(filePath: string, namespace?: string | null): Promise<ArchiveAccessor>;
136
352
  /**
137
353
  * Creates a new archive at the specified file path.
138
354
  * Initializes a temporary working directory and a fresh SQLite database.
@@ -167,6 +383,28 @@ export default class Archive extends ArchiveAccessor {
167
383
  * @returns A formatted timestamp string.
168
384
  */
169
385
  static timestamp(): string;
386
+ /**
387
+ * Closes the archive. If the archive file does not yet exist on disk,
388
+ * it writes the archive first. If the temporary directory still exists,
389
+ * it is removed. The database connection is then closed via
390
+ * {@link ArchiveAccessor.close} (the base class owns the SQLite handle),
391
+ * and finally the archive's advisory lock is released.
392
+ *
393
+ * **Idempotent**: the first invocation captures the close promise;
394
+ * subsequent invocations (signal handlers, parallel teardowns, retried
395
+ * orchestrator paths) await the same promise instead of re-entering
396
+ * the destructive prologue on a half-mutated state. If the first
397
+ * close fails (e.g. ENOSPC during tar), the rejection propagates to
398
+ * all awaiters and the archive stays latched closed — there is no
399
+ * safe way to retry `write()` once `tmpDir` has been renamed.
400
+ *
401
+ * **Read-only consumers must not reach this override.** Anything that
402
+ * obtains an archive view via {@link Archive.connect} receives an
403
+ * {@link ArchiveAccessor} (not an `Archive`), so `close()` resolves to
404
+ * the safe base implementation — no `write()`, no `remove()`, no lock
405
+ * release — leaving the tmpDir intact for the live crawler.
406
+ */
407
+ close(): Promise<void>;
170
408
  /**
171
409
  * Retrieves the crawl configuration stored in the archive database.
172
410
  * @returns The configuration object.