@nitpicker/crawler 0.13.0 → 0.15.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 (171) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +108 -9
  4. package/lib/archive/archive.js +109 -7
  5. package/lib/archive/body-hash/compute-body-hash.d.ts +22 -0
  6. package/lib/archive/body-hash/compute-body-hash.js +31 -0
  7. package/lib/archive/body-hash/extract-body.d.ts +18 -0
  8. package/lib/archive/body-hash/extract-body.js +29 -0
  9. package/lib/archive/body-hash/mask-dynamic-ids.d.ts +33 -0
  10. package/lib/archive/body-hash/mask-dynamic-ids.js +44 -0
  11. package/lib/archive/body-hash/normalize-url-like-strings.d.ts +16 -0
  12. package/lib/archive/body-hash/normalize-url-like-strings.js +19 -0
  13. package/lib/archive/cache/clear-archive-cache-entry.d.ts +19 -0
  14. package/lib/archive/cache/clear-archive-cache-entry.js +25 -0
  15. package/lib/archive/cache/clear-archive-cache-root.d.ts +26 -0
  16. package/lib/archive/cache/clear-archive-cache-root.js +32 -0
  17. package/lib/archive/cache/list-archive-cache-entries.d.ts +23 -0
  18. package/lib/archive/cache/list-archive-cache-entries.js +125 -0
  19. package/lib/archive/cache/path-exists.d.ts +9 -0
  20. package/lib/archive/cache/path-exists.js +18 -0
  21. package/lib/archive/cache/types.d.ts +33 -0
  22. package/lib/archive/cache/types.js +1 -0
  23. package/lib/archive/create-adjunct-tables.d.ts +18 -0
  24. package/lib/archive/create-adjunct-tables.js +277 -0
  25. package/lib/archive/create-entity-tables.d.ts +14 -0
  26. package/lib/archive/create-entity-tables.js +52 -1
  27. package/lib/archive/database.d.ts +91 -2
  28. package/lib/archive/database.js +128 -0
  29. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +1 -0
  30. package/lib/archive/db-ops/_shared/compress-payload.d.ts +26 -0
  31. package/lib/archive/db-ops/_shared/compress-payload.js +30 -0
  32. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +1 -0
  33. package/lib/archive/db-ops/_shared/types.d.ts +2 -0
  34. package/lib/archive/db-ops/_shared/upsert-json-ref.js +3 -3
  35. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +29 -0
  36. package/lib/archive/db-ops/analysis/replace-page-templates.js +84 -0
  37. package/lib/archive/db-ops/analysis/types.d.ts +61 -0
  38. package/lib/archive/db-ops/analysis/types.js +1 -0
  39. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  40. package/lib/archive/db-ops/console-logs/compute-console-log-hash.d.ts +19 -0
  41. package/lib/archive/db-ops/console-logs/compute-console-log-hash.js +30 -0
  42. package/lib/archive/db-ops/console-logs/replace-console-logs.d.ts +34 -0
  43. package/lib/archive/db-ops/console-logs/replace-console-logs.js +123 -0
  44. package/lib/archive/db-ops/console-logs/stringify-console-log-args.d.ts +16 -0
  45. package/lib/archive/db-ops/console-logs/stringify-console-log-args.js +26 -0
  46. package/lib/archive/db-ops/console-logs/types.d.ts +38 -0
  47. package/lib/archive/db-ops/console-logs/types.js +1 -0
  48. package/lib/archive/db-ops/console-logs/upsert-console-log-item.d.ts +29 -0
  49. package/lib/archive/db-ops/console-logs/upsert-console-log-item.js +52 -0
  50. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +11 -0
  51. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +21 -4
  52. package/lib/archive/db-ops/inventory/record-inventory-run.js +1 -0
  53. package/lib/archive/db-ops/lifecycle/init.d.ts +25 -10
  54. package/lib/archive/db-ops/lifecycle/init.js +39 -10
  55. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  56. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  57. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  58. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  59. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  60. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  61. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  62. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  63. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  64. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  65. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  66. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  67. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  68. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  69. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  70. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  71. package/lib/archive/db-ops/outages/close-network-outage.d.ts +12 -0
  72. package/lib/archive/db-ops/outages/close-network-outage.js +15 -0
  73. package/lib/archive/db-ops/outages/close-stale-open-network-outages.d.ts +26 -0
  74. package/lib/archive/db-ops/outages/close-stale-open-network-outages.js +42 -0
  75. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.d.ts +26 -0
  76. package/lib/archive/db-ops/outages/compute-outage-clamp-timestamp.js +31 -0
  77. package/lib/archive/db-ops/outages/insert-network-outage.d.ts +14 -0
  78. package/lib/archive/db-ops/outages/insert-network-outage.js +29 -0
  79. package/lib/archive/db-ops/outages/list-network-outages.d.ts +28 -0
  80. package/lib/archive/db-ops/outages/list-network-outages.js +42 -0
  81. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  82. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  83. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +18 -3
  84. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +48 -10
  85. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  86. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  87. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  88. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  89. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  90. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  91. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  92. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  93. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  94. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  95. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  96. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  97. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  98. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  99. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  100. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  101. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  102. package/lib/archive/db-ops/pages/write/update-page.js +33 -0
  103. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +6 -6
  104. package/lib/archive/filesystem/output-binary.d.ts +12 -0
  105. package/lib/archive/filesystem/output-binary.js +17 -0
  106. package/lib/archive/get-failed-page-messages.d.ts +20 -8
  107. package/lib/archive/get-failed-page-messages.js +29 -15
  108. package/lib/archive/init-schema.js +2 -1
  109. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  110. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  111. package/lib/archive/meta/types.d.ts +263 -0
  112. package/lib/archive/migrate-content-items-alias-of-id.d.ts +47 -0
  113. package/lib/archive/migrate-content-items-alias-of-id.js +58 -0
  114. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  115. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  116. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +11 -0
  117. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +24 -0
  118. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  119. package/lib/archive/migrate-main-contents-columns.js +50 -0
  120. package/lib/archive/migrate-page-meta-body-hash.d.ts +39 -0
  121. package/lib/archive/migrate-page-meta-body-hash.js +52 -0
  122. package/lib/archive/migrate-page-meta-console-error-count.d.ts +22 -0
  123. package/lib/archive/migrate-page-meta-console-error-count.js +34 -0
  124. package/lib/archive/page.d.ts +145 -3
  125. package/lib/archive/page.js +195 -0
  126. package/lib/archive/types.d.ts +86 -2
  127. package/lib/archive/url-alias/compute-tier-a-alias-key.d.ts +31 -0
  128. package/lib/archive/url-alias/compute-tier-a-alias-key.js +36 -0
  129. package/lib/archive/url-alias/compute-tier-b-alias-key.d.ts +19 -0
  130. package/lib/archive/url-alias/compute-tier-b-alias-key.js +30 -0
  131. package/lib/archive/url-alias/format-alias-key.d.ts +13 -0
  132. package/lib/archive/url-alias/format-alias-key.js +14 -0
  133. package/lib/archive/url-alias/parse-alias-key-parts.d.ts +15 -0
  134. package/lib/archive/url-alias/parse-alias-key-parts.js +40 -0
  135. package/lib/archive/url-alias/types.d.ts +7 -0
  136. package/lib/archive/url-alias/types.js +1 -0
  137. package/lib/crawler/choose-probe-host.d.ts +24 -0
  138. package/lib/crawler/choose-probe-host.js +38 -0
  139. package/lib/crawler/clear-dns-burned-host-cache.js +2 -0
  140. package/lib/crawler/crawler.js +261 -2
  141. package/lib/crawler/dns-burned-host-burn-timestamps.d.ts +17 -0
  142. package/lib/crawler/dns-burned-host-burn-timestamps.js +17 -0
  143. package/lib/crawler/evict-network-classified-destination-cache-entries.d.ts +26 -0
  144. package/lib/crawler/evict-network-classified-destination-cache-entries.js +34 -0
  145. package/lib/crawler/evict-outage-tainted-dns-burns.d.ts +23 -0
  146. package/lib/crawler/evict-outage-tainted-dns-burns.js +26 -0
  147. package/lib/crawler/fetch-destination.js +2 -0
  148. package/lib/crawler/link-to-page-data.js +2 -0
  149. package/lib/crawler/network-gate.d.ts +49 -0
  150. package/lib/crawler/network-gate.js +78 -0
  151. package/lib/crawler/network-outage-detector.d.ts +51 -0
  152. package/lib/crawler/network-outage-detector.js +81 -0
  153. package/lib/crawler/network-outage-summary-counter.d.ts +21 -0
  154. package/lib/crawler/network-outage-summary-counter.js +18 -0
  155. package/lib/crawler/probe-network.d.ts +26 -0
  156. package/lib/crawler/probe-network.js +21 -0
  157. package/lib/crawler/resource-to-page-data.js +2 -0
  158. package/lib/crawler/types.d.ts +152 -1
  159. package/lib/crawler-orchestrator.d.ts +62 -18
  160. package/lib/crawler-orchestrator.js +163 -26
  161. package/lib/crawler.d.ts +21 -0
  162. package/lib/crawler.js +17 -0
  163. package/lib/is-within-outage-window.d.ts +49 -0
  164. package/lib/is-within-outage-window.js +33 -0
  165. package/lib/network-related-error-kinds.d.ts +23 -0
  166. package/lib/network-related-error-kinds.js +28 -0
  167. package/lib/types.d.ts +9 -1
  168. package/lib/utils/array/each-splitted.d.ts +1 -1
  169. package/lib/utils/compute-file-sha256.d.ts +17 -17
  170. package/lib/utils/compute-file-sha256.js +18 -47
  171. package/package.json +3 -3
@@ -0,0 +1,22 @@
1
+ import type { MainContentsDenormalizedColumns } from './types.js';
2
+ import type { MainContentsData, ScrollHeightData } from '@d-zero/beholder';
3
+ /**
4
+ * Computes the `page_meta.main_content_*` / `scroll_height_*` denormalised
5
+ * aggregate columns from beholder's `MainContentsData` / `ScrollHeightData`.
6
+ *
7
+ * `mainContents` is `null` for pages that were not fully rendered (external,
8
+ * non-HTML, or metadata-only scrapes) — in that case every column is `null`.
9
+ * This mirrors {@link import('./compute-page-denormalized.js').computePageDenormalized}'s
10
+ * write-once-at-scrape-time pattern so list / detail reads never re-derive
11
+ * counts from the `page_main_content_*` child tables.
12
+ *
13
+ * Accepts `undefined` as well as `null`: `PageData` declares `mainContents`
14
+ * as required, but test fixtures across the codebase predate this field and
15
+ * omit it (`.spec.ts` files are excluded from the `tsc` build, so this goes
16
+ * uncaught at compile time) — `== null` tolerates both without forcing every
17
+ * fixture to be updated.
18
+ * @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
19
+ * @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
20
+ * @returns The seventeen denormalised columns.
21
+ */
22
+ export declare function computeMainContentsDenormalized(mainContents: MainContentsData | null | undefined, scrollHeight: ScrollHeightData | null | undefined): MainContentsDenormalizedColumns;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Computes the `page_meta.main_content_*` / `scroll_height_*` denormalised
3
+ * aggregate columns from beholder's `MainContentsData` / `ScrollHeightData`.
4
+ *
5
+ * `mainContents` is `null` for pages that were not fully rendered (external,
6
+ * non-HTML, or metadata-only scrapes) — in that case every column is `null`.
7
+ * This mirrors {@link import('./compute-page-denormalized.js').computePageDenormalized}'s
8
+ * write-once-at-scrape-time pattern so list / detail reads never re-derive
9
+ * counts from the `page_main_content_*` child tables.
10
+ *
11
+ * Accepts `undefined` as well as `null`: `PageData` declares `mainContents`
12
+ * as required, but test fixtures across the codebase predate this field and
13
+ * omit it (`.spec.ts` files are excluded from the `tsc` build, so this goes
14
+ * uncaught at compile time) — `== null` tolerates both without forcing every
15
+ * fixture to be updated.
16
+ * @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
17
+ * @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
18
+ * @returns The seventeen denormalised columns.
19
+ */
20
+ export function computeMainContentsDenormalized(mainContents, scrollHeight) {
21
+ if (mainContents == null) {
22
+ return {
23
+ main_content_node_name: null,
24
+ main_content_id: null,
25
+ main_content_role: null,
26
+ main_content_selector: null,
27
+ main_content_class_list: null,
28
+ main_content_word_count: null,
29
+ main_content_body_word_count: null,
30
+ main_content_heading_count: null,
31
+ main_content_image_count: null,
32
+ main_content_table_count: null,
33
+ main_content_button_count: null,
34
+ main_content_iframe_count: null,
35
+ main_content_video_count: null,
36
+ main_content_audio_count: null,
37
+ main_content_canvas_count: null,
38
+ scroll_height_desktop: null,
39
+ scroll_height_mobile: null,
40
+ };
41
+ }
42
+ return {
43
+ main_content_node_name: mainContents.main?.nodeName ?? null,
44
+ main_content_id: mainContents.main?.id ?? null,
45
+ main_content_role: mainContents.main?.role ?? null,
46
+ main_content_selector: mainContents.main?.selector ?? null,
47
+ main_content_class_list: mainContents.main
48
+ ? JSON.stringify(mainContents.main.classList)
49
+ : null,
50
+ main_content_word_count: mainContents.wordCount,
51
+ main_content_body_word_count: mainContents.bodyWordCount,
52
+ main_content_heading_count: mainContents.headings.length,
53
+ main_content_image_count: mainContents.images.length,
54
+ main_content_table_count: mainContents.tables.length,
55
+ main_content_button_count: mainContents.buttons.length,
56
+ main_content_iframe_count: mainContents.iframes.length,
57
+ main_content_video_count: mainContents.videos.length,
58
+ main_content_audio_count: mainContents.audios.length,
59
+ main_content_canvas_count: mainContents.canvases.length,
60
+ scroll_height_desktop: scrollHeight?.desktop ?? null,
61
+ scroll_height_mobile: scrollHeight?.mobile ?? null,
62
+ };
63
+ }
@@ -93,6 +93,269 @@ export interface PageDenormalizedColumns {
93
93
  /** Sorted unique providers, comma-separated (empty string when no tags). */
94
94
  tags_providers_csv: string;
95
95
  }
96
+ /**
97
+ * Denormalised aggregates computed at write time from beholder's
98
+ * `MainContentsData` / `ScrollHeightData`.
99
+ *
100
+ * Stored on `page_meta` following the same pattern as
101
+ * {@link PageDenormalizedColumns} (`tag_count` / `jsonld_count`): the full
102
+ * per-element detail lives in the `page_main_content_*` child tables, while
103
+ * these scalar columns let list / detail reads answer "how many headings?"
104
+ * without joining them. All fields are `null` when the page was not fully
105
+ * rendered (external / non-HTML / metadata-only scrape) — see
106
+ * `compute-main-contents-denormalized.ts` for the `null`-in-null-out contract.
107
+ * @see compute-main-contents-denormalized.ts
108
+ * @example
109
+ * const denorm: MainContentsDenormalizedColumns = {
110
+ * main_content_node_name: 'MAIN',
111
+ * main_content_id: null,
112
+ * main_content_role: null,
113
+ * main_content_selector: 'main.l-main',
114
+ * main_content_class_list: '["l-main"]',
115
+ * main_content_word_count: 1240,
116
+ * main_content_body_word_count: 1580,
117
+ * main_content_heading_count: 6,
118
+ * main_content_image_count: 3,
119
+ * main_content_table_count: 0,
120
+ * main_content_button_count: 1,
121
+ * main_content_iframe_count: 0,
122
+ * main_content_video_count: 0,
123
+ * main_content_audio_count: 0,
124
+ * main_content_canvas_count: 0,
125
+ * scroll_height_desktop: 3200,
126
+ * scroll_height_mobile: 5400,
127
+ * };
128
+ */
129
+ export interface MainContentsDenormalizedColumns {
130
+ /** Detected main-content element's `nodeName` (e.g. `'MAIN'`), or `null`. */
131
+ main_content_node_name: string | null;
132
+ /** Detected main-content element's `id`, or `null`. */
133
+ main_content_id: string | null;
134
+ /** Detected main-content element's `role` attribute, or `null`. */
135
+ main_content_role: string | null;
136
+ /** Diagnostic tag+id+class selector for the detected element, or `null`. */
137
+ main_content_selector: string | null;
138
+ /** JSON-encoded array of the detected element's CSS classes, or `null`. */
139
+ main_content_class_list: string | null;
140
+ /** Character count of the main region's text content, or `null`. */
141
+ main_content_word_count: number | null;
142
+ /** Character count of `document.body`'s text content, or `null`. */
143
+ main_content_body_word_count: number | null;
144
+ /** Number of headings within the main region, or `null`. */
145
+ main_content_heading_count: number | null;
146
+ /** Number of images within the main region, or `null`. */
147
+ main_content_image_count: number | null;
148
+ /** Number of tables within the main region, or `null`. */
149
+ main_content_table_count: number | null;
150
+ /** Number of button-like elements within the main region, or `null`. */
151
+ main_content_button_count: number | null;
152
+ /** Number of iframes within the main region, or `null`. */
153
+ main_content_iframe_count: number | null;
154
+ /** Number of videos within the main region, or `null`. */
155
+ main_content_video_count: number | null;
156
+ /** Number of audios within the main region, or `null`. */
157
+ main_content_audio_count: number | null;
158
+ /** Number of canvases within the main region, or `null`. */
159
+ main_content_canvas_count: number | null;
160
+ /** `document.body.scrollHeight` at the desktop-compact preset, or `null`. */
161
+ scroll_height_desktop: number | null;
162
+ /** `document.body.scrollHeight` at the mobile-small preset, or `null`. */
163
+ scroll_height_mobile: number | null;
164
+ }
165
+ /**
166
+ * One row in the `page_main_content_headings` table.
167
+ * @example
168
+ * const row: MainContentHeadingRow = { id: 1, pageId: 42, order: 0, text: 'Welcome', level: 1 };
169
+ */
170
+ export interface MainContentHeadingRow {
171
+ /** Auto-increment primary key. */
172
+ id: number;
173
+ /** FK to `content_items.id`. */
174
+ pageId: number;
175
+ /** 0-based DOM traversal order within the main content region. */
176
+ order: number;
177
+ /** Heading text after whitespace removal, or `null` when empty. */
178
+ text: string | null;
179
+ /** Heading level (1-6) from the tag name. */
180
+ level: 1 | 2 | 3 | 4 | 5 | 6;
181
+ }
182
+ /**
183
+ * One row in the `page_main_content_images` table.
184
+ * @example
185
+ * const row: MainContentImageRow = {
186
+ * id: 1,
187
+ * pageId: 42,
188
+ * order: 0,
189
+ * src: 'https://example.com/a.png',
190
+ * alt: 'A photo',
191
+ * };
192
+ */
193
+ export interface MainContentImageRow {
194
+ /** Auto-increment primary key. */
195
+ id: number;
196
+ /** FK to `content_items.id`. */
197
+ pageId: number;
198
+ /** 0-based DOM traversal order within the main content region. */
199
+ order: number;
200
+ /** Resolved absolute `src` URL. */
201
+ src: string;
202
+ /** `alt` attribute value (may be an empty string). */
203
+ alt: string;
204
+ }
205
+ /**
206
+ * One row in the `page_main_content_tables` table.
207
+ * @example
208
+ * const row: MainContentTableRow = {
209
+ * id: 1,
210
+ * pageId: 42,
211
+ * order: 0,
212
+ * rows: 3,
213
+ * cols: 4,
214
+ * hasHeader: 1,
215
+ * hasFooter: 0,
216
+ * hasMergedCell: 0,
217
+ * };
218
+ */
219
+ export interface MainContentTableRow {
220
+ /** Auto-increment primary key. */
221
+ id: number;
222
+ /** FK to `content_items.id`. */
223
+ pageId: number;
224
+ /** 0-based DOM traversal order within the main content region. */
225
+ order: number;
226
+ /** Number of `<tr>` elements. */
227
+ rows: number;
228
+ /** Number of `th`/`td` cells in the first row. */
229
+ cols: number;
230
+ /** Whether the table contains a `<thead>` (raw SQLite 0/1; knex does not round-trip `.boolean()` columns back to JS `boolean` on read). */
231
+ hasHeader: 0 | 1;
232
+ /** Whether the table contains a `<tfoot>` (raw SQLite 0/1). */
233
+ hasFooter: 0 | 1;
234
+ /** Whether any cell uses `colspan` or `rowspan` (raw SQLite 0/1). */
235
+ hasMergedCell: 0 | 1;
236
+ }
237
+ /**
238
+ * One row in the `page_main_content_buttons` table.
239
+ * @example
240
+ * const row: MainContentButtonRow = {
241
+ * id: 1,
242
+ * pageId: 42,
243
+ * order: 0,
244
+ * nodeName: 'BUTTON',
245
+ * role: null,
246
+ * type: 'submit',
247
+ * text: 'Send',
248
+ * disabled: 0,
249
+ * };
250
+ */
251
+ export interface MainContentButtonRow {
252
+ /** Auto-increment primary key. */
253
+ id: number;
254
+ /** FK to `content_items.id`. */
255
+ pageId: number;
256
+ /** 0-based DOM traversal order within the main content region. */
257
+ order: number;
258
+ /** Element tag name (e.g. `'BUTTON'`, `'A'`, `'DIV'`). */
259
+ nodeName: string;
260
+ /** `role` attribute, or `null` when absent. */
261
+ role: string | null;
262
+ /** `type` for `<button>` / `<input>`, otherwise `null`. */
263
+ type: string | null;
264
+ /** Label text after whitespace removal, or `null` when empty. */
265
+ text: string | null;
266
+ /** `true` when `disabled` or `aria-disabled="true"` (raw SQLite 0/1). */
267
+ disabled: 0 | 1;
268
+ }
269
+ /**
270
+ * One row in the `page_main_content_iframes` table.
271
+ * @example
272
+ * const row: MainContentIframeRow = {
273
+ * id: 1,
274
+ * pageId: 42,
275
+ * order: 0,
276
+ * src: 'https://example.com/embed',
277
+ * title: null,
278
+ * width: '640',
279
+ * height: '360',
280
+ * };
281
+ */
282
+ export interface MainContentIframeRow {
283
+ /** Auto-increment primary key. */
284
+ id: number;
285
+ /** FK to `content_items.id`. */
286
+ pageId: number;
287
+ /** 0-based DOM traversal order within the main content region. */
288
+ order: number;
289
+ /** Resolved absolute `src` URL. */
290
+ src: string;
291
+ /** `title` attribute, or `null` when absent. */
292
+ title: string | null;
293
+ /** Raw `width` attribute string, or `null` when absent. */
294
+ width: string | null;
295
+ /** Raw `height` attribute string, or `null` when absent. */
296
+ height: string | null;
297
+ }
298
+ /**
299
+ * One row in the `page_main_content_videos` table.
300
+ * @example
301
+ * const row: MainContentVideoRow = {
302
+ * id: 1,
303
+ * pageId: 42,
304
+ * order: 0,
305
+ * src: 'https://example.com/v.mp4',
306
+ * poster: null,
307
+ * width: 640,
308
+ * height: 360,
309
+ * };
310
+ */
311
+ export interface MainContentVideoRow {
312
+ /** Auto-increment primary key. */
313
+ id: number;
314
+ /** FK to `content_items.id`. */
315
+ pageId: number;
316
+ /** 0-based DOM traversal order within the main content region. */
317
+ order: number;
318
+ /** Resolved media URL. */
319
+ src: string;
320
+ /** Resolved `poster` URL, or `null` when unset. */
321
+ poster: string | null;
322
+ /** IDL `width` in pixels. */
323
+ width: number;
324
+ /** IDL `height` in pixels. */
325
+ height: number;
326
+ }
327
+ /**
328
+ * One row in the `page_main_content_audios` table.
329
+ * @example
330
+ * const row: MainContentAudioRow = { id: 1, pageId: 42, order: 0, src: 'https://example.com/a.mp3' };
331
+ */
332
+ export interface MainContentAudioRow {
333
+ /** Auto-increment primary key. */
334
+ id: number;
335
+ /** FK to `content_items.id`. */
336
+ pageId: number;
337
+ /** 0-based DOM traversal order within the main content region. */
338
+ order: number;
339
+ /** Resolved media URL. */
340
+ src: string;
341
+ }
342
+ /**
343
+ * One row in the `page_main_content_canvases` table.
344
+ * @example
345
+ * const row: MainContentCanvasRow = { id: 1, pageId: 42, order: 0, width: 300, height: 150 };
346
+ */
347
+ export interface MainContentCanvasRow {
348
+ /** Auto-increment primary key. */
349
+ id: number;
350
+ /** FK to `content_items.id`. */
351
+ pageId: number;
352
+ /** 0-based DOM traversal order within the main content region. */
353
+ order: number;
354
+ /** IDL bitmap width. */
355
+ width: number;
356
+ /** IDL bitmap height. */
357
+ height: number;
358
+ }
96
359
  /**
97
360
  * One row in the `page_jsonld` table.
98
361
  *
@@ -0,0 +1,47 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `content_items.alias_of_id` column to archives created before
4
+ * this feature, then ensures its index exists.
5
+ *
6
+ * `content_items` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
7
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
8
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
9
+ * against an *existing* table — adding a column to the DDL string never
10
+ * reaches an archive whose `content_items` predates this change. This
11
+ * mirrors {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}'s
12
+ * catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
13
+ * `CREATE TABLE IF NOT EXISTS` cannot retrofit.
14
+ *
15
+ * Uses a raw `ALTER TABLE` (not the knex schema builder) so the retrofitted
16
+ * column's `REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED`
17
+ * constraint matches the fresh-archive DDL bit-for-bit — the same
18
+ * self-referencing shape as `redirect_dest_id` (see
19
+ * `create-entity-tables.ts`'s JSDoc on why that FK must be deferred: a
20
+ * lower-id row can reference a higher-id row within the same write).
21
+ *
22
+ * The index is created here — unconditionally, after the column-add guard,
23
+ * not inside `createEntityTables`'s DDL — for both a fresh archive (where
24
+ * `alias_of_id` already exists from the DDL, so only the index still needs
25
+ * creating) and a legacy archive (where the column is added just above,
26
+ * then the index follows in the same call). `createEntityTables` runs
27
+ * unconditionally on every archive open, including legacy archives that
28
+ * still lack `alias_of_id` at that point; an unconditional
29
+ * `CREATE INDEX ... alias_of_id` there would fail with `no such column`
30
+ * before this migration ever runs. This function is the one place
31
+ * guaranteed to run only after the column is confirmed present, for both
32
+ * archive kinds.
33
+ *
34
+ * Only adds the column and its index — it does not compute values for
35
+ * existing rows (they stay `NULL`). That computation runs separately, from
36
+ * `backfillAliasOfId` during a viewer-read-model build, since it requires
37
+ * comparing every page's title and (for the trailing-slash tier) its
38
+ * `body_hash` against every other page.
39
+ *
40
+ * Idempotent: adding the column is a no-op once it exists (the index
41
+ * creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
42
+ * Guards on `content_items`'s existence defensively, though by the time this
43
+ * runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
44
+ * pre-0.13 archives) the table is always present.
45
+ * @param instance - The Knex query builder instance connected to the database.
46
+ */
47
+ export declare function migrateContentItemsAliasOfId(instance: Knex): Promise<void>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Adds the `content_items.alias_of_id` column to archives created before
3
+ * this feature, then ensures its index exists.
4
+ *
5
+ * `content_items` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
6
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
7
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
8
+ * against an *existing* table — adding a column to the DDL string never
9
+ * reaches an archive whose `content_items` predates this change. This
10
+ * mirrors {@link import('./migrate-page-meta-body-hash.js').migratePageMetaBodyHash}'s
11
+ * catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
12
+ * `CREATE TABLE IF NOT EXISTS` cannot retrofit.
13
+ *
14
+ * Uses a raw `ALTER TABLE` (not the knex schema builder) so the retrofitted
15
+ * column's `REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED`
16
+ * constraint matches the fresh-archive DDL bit-for-bit — the same
17
+ * self-referencing shape as `redirect_dest_id` (see
18
+ * `create-entity-tables.ts`'s JSDoc on why that FK must be deferred: a
19
+ * lower-id row can reference a higher-id row within the same write).
20
+ *
21
+ * The index is created here — unconditionally, after the column-add guard,
22
+ * not inside `createEntityTables`'s DDL — for both a fresh archive (where
23
+ * `alias_of_id` already exists from the DDL, so only the index still needs
24
+ * creating) and a legacy archive (where the column is added just above,
25
+ * then the index follows in the same call). `createEntityTables` runs
26
+ * unconditionally on every archive open, including legacy archives that
27
+ * still lack `alias_of_id` at that point; an unconditional
28
+ * `CREATE INDEX ... alias_of_id` there would fail with `no such column`
29
+ * before this migration ever runs. This function is the one place
30
+ * guaranteed to run only after the column is confirmed present, for both
31
+ * archive kinds.
32
+ *
33
+ * Only adds the column and its index — it does not compute values for
34
+ * existing rows (they stay `NULL`). That computation runs separately, from
35
+ * `backfillAliasOfId` during a viewer-read-model build, since it requires
36
+ * comparing every page's title and (for the trailing-slash tier) its
37
+ * `body_hash` against every other page.
38
+ *
39
+ * Idempotent: adding the column is a no-op once it exists (the index
40
+ * creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
41
+ * Guards on `content_items`'s existence defensively, though by the time this
42
+ * runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
43
+ * pre-0.13 archives) the table is always present.
44
+ * @param instance - The Knex query builder instance connected to the database.
45
+ */
46
+ export async function migrateContentItemsAliasOfId(instance) {
47
+ const hasContentItems = await instance.schema.hasTable('content_items');
48
+ if (!hasContentItems) {
49
+ return;
50
+ }
51
+ const hasColumn = await instance.schema.hasColumn('content_items', 'alias_of_id');
52
+ if (!hasColumn) {
53
+ await instance.raw('ALTER TABLE content_items ADD COLUMN alias_of_id INTEGER REFERENCES content_items(id) DEFERRABLE INITIALLY DEFERRED');
54
+ // eslint-disable-next-line no-console
55
+ console.error('[migrate] content_items.alias_of_id column added');
56
+ }
57
+ await instance.raw('CREATE INDEX IF NOT EXISTS idx_content_items_alias_of_id ON content_items(alias_of_id)');
58
+ }
@@ -0,0 +1,11 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `info.mainContentSelector` column to archives created before it
4
+ * existed. `CREATE TABLE IF NOT EXISTS` (used for `info` itself) cannot
5
+ * retrofit a new column onto an already-existing table, so this lightweight,
6
+ * `hasColumn`-guarded `ALTER TABLE` runs on every `initSchema` call —
7
+ * idempotent, and self-healing for archives whose provisioning crashed
8
+ * partway through.
9
+ * @param instance - The Knex query builder instance connected to the database.
10
+ */
11
+ export declare function migrateInfoMainContentSelector(instance: Knex): Promise<void>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Adds the `info.mainContentSelector` column to archives created before it
3
+ * existed. `CREATE TABLE IF NOT EXISTS` (used for `info` itself) cannot
4
+ * retrofit a new column onto an already-existing table, so this lightweight,
5
+ * `hasColumn`-guarded `ALTER TABLE` runs on every `initSchema` call —
6
+ * idempotent, and self-healing for archives whose provisioning crashed
7
+ * partway through.
8
+ * @param instance - The Knex query builder instance connected to the database.
9
+ */
10
+ export async function migrateInfoMainContentSelector(instance) {
11
+ const hasInfo = await instance.schema.hasTable('info');
12
+ if (!hasInfo) {
13
+ return;
14
+ }
15
+ const hasColumn = await instance.schema.hasColumn('info', 'mainContentSelector');
16
+ if (hasColumn) {
17
+ return;
18
+ }
19
+ await instance.schema.table('info', (t) => {
20
+ t.string('mainContentSelector');
21
+ });
22
+ // eslint-disable-next-line no-console
23
+ console.error('[migrate] info.mainContentSelector column added');
24
+ }
@@ -0,0 +1,11 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `inventory_runs.invalid_skipped` column to archives created
4
+ * before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
5
+ * itself) cannot retrofit a new column onto an already-existing table, so
6
+ * this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
7
+ * `initSchema` call — idempotent, and self-healing for archives whose
8
+ * provisioning crashed partway through.
9
+ * @param instance - The Knex query builder instance connected to the database.
10
+ */
11
+ export declare function migrateInventoryRunsInvalidSkipped(instance: Knex): Promise<void>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Adds the `inventory_runs.invalid_skipped` column to archives created
3
+ * before it existed. `CREATE TABLE IF NOT EXISTS` (used for `inventory_runs`
4
+ * itself) cannot retrofit a new column onto an already-existing table, so
5
+ * this lightweight, `hasColumn`-guarded `ALTER TABLE` runs on every
6
+ * `initSchema` call — idempotent, and self-healing for archives whose
7
+ * provisioning crashed partway through.
8
+ * @param instance - The Knex query builder instance connected to the database.
9
+ */
10
+ export async function migrateInventoryRunsInvalidSkipped(instance) {
11
+ const hasTable = await instance.schema.hasTable('inventory_runs');
12
+ if (!hasTable) {
13
+ return;
14
+ }
15
+ const hasColumn = await instance.schema.hasColumn('inventory_runs', 'invalid_skipped');
16
+ if (hasColumn) {
17
+ return;
18
+ }
19
+ await instance.schema.table('inventory_runs', (t) => {
20
+ t.integer('invalid_skipped');
21
+ });
22
+ // eslint-disable-next-line no-console
23
+ console.error('[migrate] inventory_runs.invalid_skipped column added');
24
+ }
@@ -0,0 +1,21 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Add the beholder `MainContentsData` / `ScrollHeightData` denormalised
4
+ * aggregate columns to `page_meta` on archives created before this feature.
5
+ *
6
+ * `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
7
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
8
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
9
+ * against an *existing* table — adding a column to the DDL string never
10
+ * reaches an archive whose `page_meta` predates this change. This mirrors
11
+ * {@link import('./migrate-info-roots.js').migrateInfoRoots}'s `info.roots`
12
+ * catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column set that
13
+ * `CREATE TABLE IF NOT EXISTS` cannot retrofit.
14
+ *
15
+ * Idempotent: a no-op once the columns exist. Guards on `page_meta`'s
16
+ * existence defensively, though by the time this runs (after `initSchema`,
17
+ * itself after `assertCompatibleVersion` rejects pre-0.13 archives) the
18
+ * table is always present.
19
+ * @param instance - The Knex query builder instance connected to the database.
20
+ */
21
+ export declare function migrateMainContentsColumns(instance: Knex): Promise<void>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Add the beholder `MainContentsData` / `ScrollHeightData` denormalised
3
+ * aggregate columns to `page_meta` on archives created before this feature.
4
+ *
5
+ * `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
6
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
7
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
8
+ * against an *existing* table — adding a column to the DDL string never
9
+ * reaches an archive whose `page_meta` predates this change. This mirrors
10
+ * {@link import('./migrate-info-roots.js').migrateInfoRoots}'s `info.roots`
11
+ * catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column set that
12
+ * `CREATE TABLE IF NOT EXISTS` cannot retrofit.
13
+ *
14
+ * Idempotent: a no-op once the columns exist. Guards on `page_meta`'s
15
+ * existence defensively, though by the time this runs (after `initSchema`,
16
+ * itself after `assertCompatibleVersion` rejects pre-0.13 archives) the
17
+ * table is always present.
18
+ * @param instance - The Knex query builder instance connected to the database.
19
+ */
20
+ export async function migrateMainContentsColumns(instance) {
21
+ const hasPageMeta = await instance.schema.hasTable('page_meta');
22
+ if (!hasPageMeta) {
23
+ return;
24
+ }
25
+ const hasColumn = await instance.schema.hasColumn('page_meta', 'main_content_word_count');
26
+ if (hasColumn) {
27
+ return;
28
+ }
29
+ await instance.schema.table('page_meta', (t) => {
30
+ t.string('main_content_node_name');
31
+ t.string('main_content_id');
32
+ t.string('main_content_role');
33
+ t.string('main_content_selector');
34
+ t.text('main_content_class_list');
35
+ t.integer('main_content_word_count');
36
+ t.integer('main_content_body_word_count');
37
+ t.integer('main_content_heading_count');
38
+ t.integer('main_content_image_count');
39
+ t.integer('main_content_table_count');
40
+ t.integer('main_content_button_count');
41
+ t.integer('main_content_iframe_count');
42
+ t.integer('main_content_video_count');
43
+ t.integer('main_content_audio_count');
44
+ t.integer('main_content_canvas_count');
45
+ t.integer('scroll_height_desktop');
46
+ t.integer('scroll_height_mobile');
47
+ });
48
+ // eslint-disable-next-line no-console
49
+ console.error('[migrate] page_meta.main_content_* / scroll_height_* columns added');
50
+ }
@@ -0,0 +1,39 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `page_meta.body_hash` column to archives created before this
4
+ * feature, then ensures its index exists.
5
+ *
6
+ * `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
7
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
8
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
9
+ * against an *existing* table — adding a column to the DDL string never
10
+ * reaches an archive whose `page_meta` predates this change. This mirrors
11
+ * {@link import('./migrate-main-contents-columns.js').migrateMainContentsColumns}'s
12
+ * catch-up: a `hasColumn`-guarded `ALTER TABLE` for the one column
13
+ * `CREATE TABLE IF NOT EXISTS` cannot retrofit.
14
+ *
15
+ * The index is created here — unconditionally, after the column-add guard,
16
+ * not inside `createEntityTables`'s DDL — for both a fresh archive (where
17
+ * `body_hash` already exists from the DDL, so only the index still needs
18
+ * creating) and a legacy archive (where the column is added just above,
19
+ * then the index follows in the same call). `createEntityTables` runs
20
+ * unconditionally on every archive open, including legacy archives that
21
+ * still lack `body_hash` at that point; an unconditional
22
+ * `CREATE INDEX ... body_hash` there would fail with `no such column` before
23
+ * this migration ever runs. This function is the one place guaranteed to
24
+ * run only after the column is confirmed present, for both archive kinds.
25
+ *
26
+ * Only adds the column and its index — it does not backfill values for
27
+ * existing rows (they stay `NULL`). That backfill runs separately, from
28
+ * `backfillBodyHashFromHtmlBlobs` during a viewer-read-model build, since it
29
+ * requires decompressing every page's stored HTML and is too heavy to run on
30
+ * every archive open.
31
+ *
32
+ * Idempotent: adding the column is a no-op once it exists (the index
33
+ * creation always runs, but `IF NOT EXISTS` makes repeat runs a no-op too).
34
+ * Guards on `page_meta`'s existence defensively, though by the time this
35
+ * runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
36
+ * pre-0.13 archives) the table is always present.
37
+ * @param instance - The Knex query builder instance connected to the database.
38
+ */
39
+ export declare function migratePageMetaBodyHash(instance: Knex): Promise<void>;