@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
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Shared type definitions for archive-side meta processing helpers under {@link ./}.
3
+ *
4
+ * Mirrors the shape of {@link import('@d-zero/beholder').Meta} after archive-side
5
+ * derivation: flat columns persisted to the `pages` table, denormalised
6
+ * aggregates, per-row shapes for `page_jsonld` / `page_tags`, and the summary
7
+ * objects returned by `get-page-detail` consumers.
8
+ * @module
9
+ */
10
+ /**
11
+ * Flat columns of the `pages` table derived from {@link import('@d-zero/beholder').Meta}.
12
+ *
13
+ * Each field maps to a single SQL column (string / number / boolean / null).
14
+ * URL-shaped columns are absolutised by the deriver before persistence so
15
+ * downstream consumers (e.g. `find-mismatches`) can compare against the
16
+ * absolute page URL directly.
17
+ * @see derive-flat-from-meta.ts
18
+ */
19
+ export interface FlatPageMetaColumns {
20
+ lang: string | null;
21
+ dir: string | null;
22
+ charset: string | null;
23
+ baseHref: string | null;
24
+ viewport_raw: string | null;
25
+ themeColor: string | null;
26
+ applicationName: string | null;
27
+ author: string | null;
28
+ generator: string | null;
29
+ publisher: string | null;
30
+ robots_raw: string | null;
31
+ robots_noindex: number | null;
32
+ robots_nofollow: number | null;
33
+ robots_noarchive: number | null;
34
+ robots_noimageindex: number | null;
35
+ googlebot: string | null;
36
+ canonical: string | null;
37
+ amphtml: string | null;
38
+ manifest: string | null;
39
+ icon_href: string | null;
40
+ appleTouchIcon_href: string | null;
41
+ og_type: string | null;
42
+ og_title: string | null;
43
+ og_url: string | null;
44
+ og_site_name: string | null;
45
+ og_description: string | null;
46
+ og_image: string | null;
47
+ og_image_alt: string | null;
48
+ og_image_width: string | null;
49
+ og_image_height: string | null;
50
+ og_locale: string | null;
51
+ og_article_published_time: string | null;
52
+ og_article_modified_time: string | null;
53
+ twitter_card: string | null;
54
+ twitter_site: string | null;
55
+ twitter_creator: string | null;
56
+ twitter_title: string | null;
57
+ twitter_description: string | null;
58
+ twitter_image: string | null;
59
+ fb_app_id: string | null;
60
+ verification_google: string | null;
61
+ formatDetection_telephone: number | null;
62
+ title: string | null;
63
+ description: string | null;
64
+ keywords: string | null;
65
+ }
66
+ /**
67
+ * Denormalised aggregates computed at write time from `meta.tags` / `meta.jsonLd`.
68
+ *
69
+ * Stored on the `pages` table so list / report read paths can avoid joining
70
+ * `page_tags` / `page_jsonld` for the common "how many?" and "which
71
+ * providers?" questions.
72
+ * @see compute-page-denormalized.ts
73
+ */
74
+ export interface PageDenormalizedColumns {
75
+ /** Total Wappalyzer tag entries for the page. */
76
+ tag_count: number;
77
+ /** `meta.jsonLd.length + meta.speculationRules.length`. */
78
+ jsonld_count: number;
79
+ /** Sorted unique providers, comma-separated (empty string when no tags). */
80
+ tags_providers_csv: string;
81
+ }
82
+ /**
83
+ * One row in the `page_jsonld` table.
84
+ *
85
+ * Captures both `<script type="application/ld+json">` (`kind = 'ld+json'`) and
86
+ * `<script type="speculationrules">` (`kind = 'speculationrules'`) entries.
87
+ * @see extract-tags-for-archive.ts (sibling for tags) and the table definition
88
+ * in `archive/init-schema.ts`.
89
+ */
90
+ export interface JsonLdRow {
91
+ /** Auto-increment primary key. */
92
+ id: number;
93
+ /** FK to `pages.id`. */
94
+ pageId: number;
95
+ /** `'ld+json'` for `application/ld+json` scripts, `'speculationrules'` for speculation rules. */
96
+ kind: 'ld+json' | 'speculationrules';
97
+ /** Top-level `@type` extracted from `parsed`, normalised to a single string. `null` when missing / unparseable. */
98
+ type: string | null;
99
+ /** Original script text content (uncompressed; SQLite overflow pages handle large rows). */
100
+ raw: string;
101
+ /** Parsed JSON object (`null` when `parseError` is set). */
102
+ parsed: unknown | null;
103
+ /** Parse error message preserved from beholder; `null` when the entry parsed cleanly. */
104
+ parseError: string | null;
105
+ }
106
+ /**
107
+ * Insert shape for {@link JsonLdRow}.
108
+ *
109
+ * Mirrors the row shape minus the auto-increment `id`. `parsed` is the raw
110
+ * JSON value (the database layer JSON-stringifies it before write).
111
+ */
112
+ export type JsonLdRowForInsert = Omit<JsonLdRow, 'id'>;
113
+ /**
114
+ * One row in the `page_tags` table.
115
+ *
116
+ * Each row represents one detected Wappalyzer provider × external-id tuple for
117
+ * one page. A page typically has 1–10 rows.
118
+ */
119
+ export interface TagRow {
120
+ /** Auto-increment primary key. */
121
+ id: number;
122
+ /** FK to `pages.id`. */
123
+ pageId: number;
124
+ /** Wappalyzer provider name (e.g. `'Google Tag Manager'`). */
125
+ provider: string;
126
+ /** First entry of `categories`. `null` when Wappalyzer did not report a category. Convenient projection only; canonical list is `categories`. */
127
+ category: string | null;
128
+ /** Real external identifier extracted by `meta/id-extractors` (e.g. `GTM-XXXX`, `G-XXXX`). `null` when none. */
129
+ externalId: string | null;
130
+ /** Wappalyzer-reported version, when available. */
131
+ version: string | null;
132
+ /** Wappalyzer-reported confidence 0–100, when available. */
133
+ confidence: number | null;
134
+ /** Full `categories` array preserved as JSON. */
135
+ categories: readonly string[];
136
+ /** `TagSource[]` preserved as JSON; describes where the provider was detected (script-src / inline / iframe-src / window-global / etc.). */
137
+ sources: ReadonlyArray<{
138
+ type: 'script-src' | 'inline' | 'iframe-src' | 'window-global' | 'img-src' | 'header' | 'meta' | 'html';
139
+ src?: string;
140
+ location?: 'head' | 'body' | 'noscript';
141
+ globalName?: string;
142
+ }>;
143
+ }
144
+ /**
145
+ * Insert shape for {@link TagRow}.
146
+ *
147
+ * Mirrors the row shape minus the auto-increment `id`. `categories` and
148
+ * `sources` are passed as plain JS arrays (the database layer JSON-stringifies
149
+ * them before write).
150
+ */
151
+ export type TagRowForInsert = Omit<TagRow, 'id'>;
152
+ /**
153
+ * Summary of one page's JSON-LD entries returned by `get-page-detail`.
154
+ *
155
+ * Keeps the response token-bounded for MCP / LLM consumers; the full `raw`
156
+ * payload is fetched separately via `get-page-jsonld(url)`.
157
+ * @see summarize-jsonld.ts
158
+ */
159
+ export interface JsonLdSummary {
160
+ /** Total entries across `ld+json` and `speculationrules`. */
161
+ count: number;
162
+ /** Unique `@type` values (sorted). `null` slots are emitted as the string `'(unknown)'`. */
163
+ types: readonly string[];
164
+ /** Number of entries that failed to parse (i.e. have a non-null `parseError`). */
165
+ parseErrorCount: number;
166
+ }
167
+ /**
168
+ * Summary of one page's Wappalyzer tags returned by `get-page-detail`.
169
+ * @see summarize-tags.ts
170
+ */
171
+ export interface TagsSummary {
172
+ /** Total tag rows for the page. */
173
+ count: number;
174
+ /** Provider → list of external IDs (unique, sorted). Providers with no IDs map to `[]`. */
175
+ providerIds: Readonly<Record<string, readonly string[]>>;
176
+ }
177
+ /**
178
+ * One row of {@link import('@d-zero/beholder').Meta.tags.detected} after
179
+ * archive-side flattening.
180
+ *
181
+ * Used by `get-tag-inventory` to return per-provider page counts across the
182
+ * whole site.
183
+ */
184
+ export interface TagInventoryEntry {
185
+ /** Wappalyzer provider name. */
186
+ provider: string;
187
+ /** Number of distinct pages where the provider was detected. */
188
+ pageCount: number;
189
+ }
190
+ /**
191
+ * Error thrown by `assert-compatible-version` when the archive's
192
+ * `info.version` is older than the format version this build accepts.
193
+ *
194
+ * Catch this at CLI / viewer boundaries to print a friendly message; do not
195
+ * confuse with generic `Error` thrown by `Database.connect` (lockfile / I/O).
196
+ */
197
+ export declare class IncompatibleArchiveError extends Error {
198
+ readonly archiveVersion: string;
199
+ readonly requiredVersion: string;
200
+ /**
201
+ * @param archiveVersion - The `info.version` value read from the archive
202
+ * (or `'unknown'` when the column is missing / null).
203
+ * @param requiredVersion - The minimum format version this build accepts
204
+ * (semver string, e.g. `'0.10.0'`).
205
+ */
206
+ constructor(archiveVersion: string, requiredVersion: string);
207
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared type definitions for archive-side meta processing helpers under {@link ./}.
3
+ *
4
+ * Mirrors the shape of {@link import('@d-zero/beholder').Meta} after archive-side
5
+ * derivation: flat columns persisted to the `pages` table, denormalised
6
+ * aggregates, per-row shapes for `page_jsonld` / `page_tags`, and the summary
7
+ * objects returned by `get-page-detail` consumers.
8
+ * @module
9
+ */
10
+ /**
11
+ * Error thrown by `assert-compatible-version` when the archive's
12
+ * `info.version` is older than the format version this build accepts.
13
+ *
14
+ * Catch this at CLI / viewer boundaries to print a friendly message; do not
15
+ * confuse with generic `Error` thrown by `Database.connect` (lockfile / I/O).
16
+ */
17
+ export class IncompatibleArchiveError extends Error {
18
+ archiveVersion;
19
+ requiredVersion;
20
+ /**
21
+ * @param archiveVersion - The `info.version` value read from the archive
22
+ * (or `'unknown'` when the column is missing / null).
23
+ * @param requiredVersion - The minimum format version this build accepts
24
+ * (semver string, e.g. `'0.10.0'`).
25
+ */
26
+ constructor(archiveVersion, requiredVersion) {
27
+ super(`Archive uses Nitpicker ${archiveVersion}; this build requires ${requiredVersion} or newer. ` +
28
+ `Run \`node scripts/migrate-to-0.10.mjs <path>\` to produce an upgraded copy next to it.`);
29
+ this.archiveVersion = archiveVersion;
30
+ this.requiredVersion = requiredVersion;
31
+ this.name = 'IncompatibleArchiveError';
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `crawl_errors` table to archives created before structured capture
4
+ * of the crawler-level `error` channel landed.
5
+ *
6
+ * `crawl_errors` records errors that previously only reached `error.log` (DNS
7
+ * failures, connection resets, TLS problems, process-level errors), in a
8
+ * queryable form so the `error-kinds` analysis can classify them without
9
+ * parsing the text log. Older `.nitpicker` files predate this table; this
10
+ * migration creates it idempotently. Existing archives stay empty until they
11
+ * are crawled again (resume / append / retry), at which point new errors are
12
+ * written here — analysis of pre-existing data falls back to parsing
13
+ * `error.log`.
14
+ *
15
+ * Idempotent: when the table already exists, the function exits without
16
+ * touching the schema or writing to stderr. It also skips empty archives (no
17
+ * `pages` table), which the regular initSchema path will fully provision.
18
+ * @param instance - The Knex query builder instance connected to the database.
19
+ */
20
+ export declare function migrateCrawlErrors(instance: Knex): Promise<void>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Adds the `crawl_errors` table to archives created before structured capture
3
+ * of the crawler-level `error` channel landed.
4
+ *
5
+ * `crawl_errors` records errors that previously only reached `error.log` (DNS
6
+ * failures, connection resets, TLS problems, process-level errors), in a
7
+ * queryable form so the `error-kinds` analysis can classify them without
8
+ * parsing the text log. Older `.nitpicker` files predate this table; this
9
+ * migration creates it idempotently. Existing archives stay empty until they
10
+ * are crawled again (resume / append / retry), at which point new errors are
11
+ * written here — analysis of pre-existing data falls back to parsing
12
+ * `error.log`.
13
+ *
14
+ * Idempotent: when the table already exists, the function exits without
15
+ * touching the schema or writing to stderr. It also skips empty archives (no
16
+ * `pages` table), which the regular initSchema path will fully provision.
17
+ * @param instance - The Knex query builder instance connected to the database.
18
+ */
19
+ export async function migrateCrawlErrors(instance) {
20
+ const hasTable = await instance.schema.hasTable('crawl_errors');
21
+ if (hasTable) {
22
+ return;
23
+ }
24
+ const hasPages = await instance.schema.hasTable('pages');
25
+ if (!hasPages) {
26
+ // Empty archive; the regular initSchema path will create the table.
27
+ return;
28
+ }
29
+ await instance.schema.createTable('crawl_errors', (t) => {
30
+ t.increments('id');
31
+ t.string('url', 8190).nullable();
32
+ t.boolean('isExternal');
33
+ t.text('message').notNullable();
34
+ t.integer('createdAt').notNullable();
35
+ });
36
+ // eslint-disable-next-line no-console
37
+ console.error('[migrate] crawl_errors table created');
38
+ }
@@ -0,0 +1,24 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the BLOB-backed HTML snapshot tables (`page_html_blobs` +
4
+ * `page_html_ref`) to archives created before #75.
5
+ *
6
+ * A pre-#75 `.nitpicker` already has the `info` table, so `initSchema`'s
7
+ * early-return path skips the freshly-introduced `CREATE TABLE`s. This
8
+ * migration brings the schema forward idempotently so the read API stops
9
+ * raising raw "no such table" errors against legacy archives. It does NOT
10
+ * touch the `pages.html` column or backfill the new tables from the
11
+ * archive's `snapshot-html.zip`: that data migration belongs to the
12
+ * `scripts/migrate-to-0.10.mjs` one-shot, which can run whenever the
13
+ * user is ready to commit the (multi-hour) CPU cost.
14
+ *
15
+ * Outcome on a legacy archive that has NOT yet been data-migrated:
16
+ * - new tables exist but are empty,
17
+ * - `getHtmlOfPageById` returns `null` for every page (no row in `page_html_ref`),
18
+ * - viewer / MCP / analyze plugins see "snapshot unavailable" instead of crashing.
19
+ *
20
+ * The migration prints a one-line notice when it runs so the user knows
21
+ * the archive was upgraded in place.
22
+ * @param instance - The Knex query builder instance connected to the database.
23
+ */
24
+ export declare function migrateHtmlBlobTables(instance: Knex): Promise<void>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Adds the BLOB-backed HTML snapshot tables (`page_html_blobs` +
3
+ * `page_html_ref`) to archives created before #75.
4
+ *
5
+ * A pre-#75 `.nitpicker` already has the `info` table, so `initSchema`'s
6
+ * early-return path skips the freshly-introduced `CREATE TABLE`s. This
7
+ * migration brings the schema forward idempotently so the read API stops
8
+ * raising raw "no such table" errors against legacy archives. It does NOT
9
+ * touch the `pages.html` column or backfill the new tables from the
10
+ * archive's `snapshot-html.zip`: that data migration belongs to the
11
+ * `scripts/migrate-to-0.10.mjs` one-shot, which can run whenever the
12
+ * user is ready to commit the (multi-hour) CPU cost.
13
+ *
14
+ * Outcome on a legacy archive that has NOT yet been data-migrated:
15
+ * - new tables exist but are empty,
16
+ * - `getHtmlOfPageById` returns `null` for every page (no row in `page_html_ref`),
17
+ * - viewer / MCP / analyze plugins see "snapshot unavailable" instead of crashing.
18
+ *
19
+ * The migration prints a one-line notice when it runs so the user knows
20
+ * the archive was upgraded in place.
21
+ * @param instance - The Knex query builder instance connected to the database.
22
+ */
23
+ export async function migrateHtmlBlobTables(instance) {
24
+ const hasBlobs = await instance.schema.hasTable('page_html_blobs');
25
+ if (hasBlobs) {
26
+ return;
27
+ }
28
+ const hasPages = await instance.schema.hasTable('pages');
29
+ if (!hasPages) {
30
+ // Empty archive; initSchema will create both tables on the
31
+ // fresh-DB path. Nothing to migrate.
32
+ return;
33
+ }
34
+ await instance.raw(`
35
+ CREATE TABLE page_html_blobs (
36
+ hash BLOB PRIMARY KEY,
37
+ body BLOB NOT NULL,
38
+ codec TEXT NOT NULL CHECK(codec IN ('zstd', 'none')),
39
+ size_raw INTEGER NOT NULL,
40
+ size_stored INTEGER NOT NULL
41
+ ) WITHOUT ROWID
42
+ `);
43
+ await instance.raw(`
44
+ CREATE TABLE page_html_ref (
45
+ page_id INTEGER PRIMARY KEY REFERENCES pages(id) ON DELETE CASCADE,
46
+ hash BLOB NOT NULL REFERENCES page_html_blobs(hash)
47
+ ) WITHOUT ROWID
48
+ `);
49
+ await instance.raw('CREATE INDEX idx_page_html_ref_hash ON page_html_ref(hash)');
50
+ // eslint-disable-next-line no-console
51
+ console.error('[migrate] page_html_blobs / page_html_ref tables created. ' +
52
+ 'HTML snapshots are empty until `node scripts/migrate-to-0.10.mjs` is run on the original archive.');
53
+ }
@@ -0,0 +1,29 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `inventory_runs` audit-log table to archives created before
4
+ * Phase 1 of inventory run tracking shipped.
5
+ *
6
+ * `inventory_runs` records one row per successful `--inventory <list>`
7
+ * invocation, capturing which deploy list was applied when and what
8
+ * scale it operated at. The motivation is operational: client /
9
+ * director conversations repeatedly ask "did you apply last month's
10
+ * list" / "we didn't double-apply, right" — the archive itself had no
11
+ * trace of inventory passes (`.bak` is unlinked on success), so this
12
+ * table is the durable provenance record.
13
+ *
14
+ * Schema details (column semantics, NULL policy, index) live in
15
+ * {@link initSchema} — this migration only re-creates the table shape
16
+ * on legacy archives so the rest of the codebase can treat the table
17
+ * as always-present once a writer connection has opened the file.
18
+ *
19
+ * Idempotent: when the table already exists, the function exits
20
+ * silently — the `[migrate] inventory_runs table created` stderr line
21
+ * fires **only** on the first run against a legacy archive, matching
22
+ * the established pattern of `migrate-page-errors.ts` /
23
+ * `migrate-crawl-errors.ts`. Operators can rely on the log line as a
24
+ * stable "first time this archive saw Phase 1 schema" event marker.
25
+ * Empty archives (no `pages` table) are skipped entirely; the regular
26
+ * `initSchema` path provisions them at first crawl.
27
+ * @param instance - The Knex query builder instance connected to the database.
28
+ */
29
+ export declare function migrateInventoryRuns(instance: Knex): Promise<void>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Adds the `inventory_runs` audit-log table to archives created before
3
+ * Phase 1 of inventory run tracking shipped.
4
+ *
5
+ * `inventory_runs` records one row per successful `--inventory <list>`
6
+ * invocation, capturing which deploy list was applied when and what
7
+ * scale it operated at. The motivation is operational: client /
8
+ * director conversations repeatedly ask "did you apply last month's
9
+ * list" / "we didn't double-apply, right" — the archive itself had no
10
+ * trace of inventory passes (`.bak` is unlinked on success), so this
11
+ * table is the durable provenance record.
12
+ *
13
+ * Schema details (column semantics, NULL policy, index) live in
14
+ * {@link initSchema} — this migration only re-creates the table shape
15
+ * on legacy archives so the rest of the codebase can treat the table
16
+ * as always-present once a writer connection has opened the file.
17
+ *
18
+ * Idempotent: when the table already exists, the function exits
19
+ * silently — the `[migrate] inventory_runs table created` stderr line
20
+ * fires **only** on the first run against a legacy archive, matching
21
+ * the established pattern of `migrate-page-errors.ts` /
22
+ * `migrate-crawl-errors.ts`. Operators can rely on the log line as a
23
+ * stable "first time this archive saw Phase 1 schema" event marker.
24
+ * Empty archives (no `pages` table) are skipped entirely; the regular
25
+ * `initSchema` path provisions them at first crawl.
26
+ * @param instance - The Knex query builder instance connected to the database.
27
+ */
28
+ export async function migrateInventoryRuns(instance) {
29
+ const hasTable = await instance.schema.hasTable('inventory_runs');
30
+ if (hasTable) {
31
+ return;
32
+ }
33
+ const hasPages = await instance.schema.hasTable('pages');
34
+ if (!hasPages) {
35
+ // Empty archive; the regular initSchema path will create the table.
36
+ return;
37
+ }
38
+ await instance.schema.createTable('inventory_runs', (t) => {
39
+ t.increments('id');
40
+ t.string('ran_at').notNullable();
41
+ t.string('list_label').nullable();
42
+ t.string('source_file_sha256', 64).nullable();
43
+ t.integer('total_lines').nullable();
44
+ t.integer('new_pages').nullable();
45
+ t.integer('new_resources').nullable();
46
+ t.integer('scope_skipped').nullable();
47
+ t.text('notes').nullable();
48
+ t.index('ran_at');
49
+ });
50
+ // eslint-disable-next-line no-console
51
+ console.error('[migrate] inventory_runs table created');
52
+ }
@@ -0,0 +1,16 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `page_errors` table to archives created before partial-failure
4
+ * recording landed.
5
+ *
6
+ * `page_errors` captures secondary scrape failures (e.g. a viewport switch
7
+ * that detaches the frame and surfaces `retryExhausted`) so they show up in
8
+ * the archive instead of being lost to stdout logs. Older `.nitpicker` files
9
+ * predate this table; this migration creates it idempotently.
10
+ *
11
+ * Idempotent: when the table already exists, the function exits without
12
+ * touching the schema or writing to stderr. When it has to run, a single
13
+ * notice is written so the user knows the file was upgraded.
14
+ * @param instance - The Knex query builder instance connected to the database.
15
+ */
16
+ export declare function migratePageErrors(instance: Knex): Promise<void>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Adds the `page_errors` table to archives created before partial-failure
3
+ * recording landed.
4
+ *
5
+ * `page_errors` captures secondary scrape failures (e.g. a viewport switch
6
+ * that detaches the frame and surfaces `retryExhausted`) so they show up in
7
+ * the archive instead of being lost to stdout logs. Older `.nitpicker` files
8
+ * predate this table; this migration creates it idempotently.
9
+ *
10
+ * Idempotent: when the table already exists, the function exits without
11
+ * touching the schema or writing to stderr. When it has to run, a single
12
+ * notice is written so the user knows the file was upgraded.
13
+ * @param instance - The Knex query builder instance connected to the database.
14
+ */
15
+ export async function migratePageErrors(instance) {
16
+ const hasTable = await instance.schema.hasTable('page_errors');
17
+ if (hasTable) {
18
+ return;
19
+ }
20
+ const hasPages = await instance.schema.hasTable('pages');
21
+ if (!hasPages) {
22
+ // Empty archive; the regular initSchema path will create both tables.
23
+ return;
24
+ }
25
+ await instance.schema.createTable('page_errors', (t) => {
26
+ t.increments('id');
27
+ t.integer('pageId').notNullable().unsigned().references('pages.id');
28
+ t.string('phase').notNullable();
29
+ t.text('message').notNullable();
30
+ t.integer('createdAt').notNullable();
31
+ t.index('pageId');
32
+ });
33
+ // eslint-disable-next-line no-console
34
+ console.error('[migrate] page_errors table created');
35
+ }
@@ -0,0 +1,16 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Add `pages.source` and `resources.source` columns (provenance taxonomy:
4
+ * `crawled` / `inventory-seed` / `inventory-discovered`) and their indexes
5
+ * to archives that pre-date the `crawl --inventory` feature.
6
+ *
7
+ * Idempotent: a no-op when both columns already exist. SQLite's
8
+ * `ALTER TABLE ADD COLUMN` with a NOT NULL DEFAULT applies the default to
9
+ * every existing row at column-add time, so no explicit `UPDATE` is needed
10
+ * to backfill — pre-existing rows become `'crawled'` automatically.
11
+ *
12
+ * Runs only on writer-side {@link Database.connect}; read-only viewer
13
+ * attaches skip this so the user's tmpDir is never rewritten.
14
+ * @param instance - The Knex query builder instance connected to the database.
15
+ */
16
+ export declare function migratePagesResourcesSource(instance: Knex): Promise<void>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Add `pages.source` and `resources.source` columns (provenance taxonomy:
3
+ * `crawled` / `inventory-seed` / `inventory-discovered`) and their indexes
4
+ * to archives that pre-date the `crawl --inventory` feature.
5
+ *
6
+ * Idempotent: a no-op when both columns already exist. SQLite's
7
+ * `ALTER TABLE ADD COLUMN` with a NOT NULL DEFAULT applies the default to
8
+ * every existing row at column-add time, so no explicit `UPDATE` is needed
9
+ * to backfill — pre-existing rows become `'crawled'` automatically.
10
+ *
11
+ * Runs only on writer-side {@link Database.connect}; read-only viewer
12
+ * attaches skip this so the user's tmpDir is never rewritten.
13
+ * @param instance - The Knex query builder instance connected to the database.
14
+ */
15
+ export async function migratePagesResourcesSource(instance) {
16
+ const hasPages = await instance.schema.hasTable('pages');
17
+ const hasResources = await instance.schema.hasTable('resources');
18
+ if (!hasPages && !hasResources) {
19
+ return;
20
+ }
21
+ const hasPagesSource = hasPages && (await instance.schema.hasColumn('pages', 'source'));
22
+ const hasResourcesSource = hasResources && (await instance.schema.hasColumn('resources', 'source'));
23
+ if (hasPagesSource && hasResourcesSource) {
24
+ return;
25
+ }
26
+ const changes = [];
27
+ if (hasPages && !hasPagesSource) {
28
+ await instance.schema.table('pages', (t) => {
29
+ t.string('source').notNullable().defaultTo('crawled');
30
+ t.index('source');
31
+ });
32
+ changes.push('pages.source added');
33
+ }
34
+ if (hasResources && !hasResourcesSource) {
35
+ await instance.schema.table('resources', (t) => {
36
+ t.string('source').notNullable().defaultTo('crawled');
37
+ t.index('source');
38
+ });
39
+ changes.push('resources.source added');
40
+ }
41
+ if (changes.length === 0) {
42
+ return;
43
+ }
44
+ // eslint-disable-next-line no-console
45
+ console.error(`[migrate] ${changes.join(', ')}`);
46
+ }