@nitpicker/crawler 0.12.0 → 0.13.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 (349) hide show
  1. package/README.md +6 -4
  2. package/lib/archive/archive-accessor.d.ts +2 -2
  3. package/lib/archive/archive-accessor.js +2 -2
  4. package/lib/archive/archive-lock.d.ts +7 -0
  5. package/lib/archive/archive-lock.js +7 -0
  6. package/lib/archive/archive.d.ts +63 -16
  7. package/lib/archive/archive.js +56 -17
  8. package/lib/archive/create-adjunct-tables.d.ts +43 -0
  9. package/lib/archive/create-adjunct-tables.js +213 -0
  10. package/lib/archive/create-entity-tables.d.ts +173 -0
  11. package/lib/archive/create-entity-tables.js +318 -0
  12. package/lib/archive/create-progress-reporter.d.ts +30 -0
  13. package/lib/archive/create-progress-reporter.js +38 -0
  14. package/lib/archive/create-ref-tables.d.ts +35 -0
  15. package/lib/archive/create-ref-tables.js +188 -0
  16. package/lib/archive/database.d.ts +92 -345
  17. package/lib/archive/database.js +168 -1942
  18. package/lib/archive/db-ops/_shared/clear-write-ref-caches.d.ts +27 -0
  19. package/lib/archive/db-ops/_shared/clear-write-ref-caches.js +34 -0
  20. package/lib/archive/db-ops/_shared/create-write-ref-caches.d.ts +17 -0
  21. package/lib/archive/db-ops/_shared/create-write-ref-caches.js +26 -0
  22. package/lib/archive/db-ops/_shared/decode-json-ref.d.ts +17 -0
  23. package/lib/archive/db-ops/_shared/decode-json-ref.js +31 -0
  24. package/lib/archive/db-ops/_shared/load-response-headers-by-set-ids.d.ts +20 -0
  25. package/lib/archive/db-ops/_shared/load-response-headers-by-set-ids.js +53 -0
  26. package/lib/archive/db-ops/_shared/resolve-content-item-id.d.ts +61 -0
  27. package/lib/archive/db-ops/_shared/resolve-content-item-id.js +111 -0
  28. package/lib/archive/db-ops/_shared/resolve-url-or-blob.d.ts +23 -0
  29. package/lib/archive/db-ops/_shared/resolve-url-or-blob.js +29 -0
  30. package/lib/archive/db-ops/_shared/retry-setting.d.ts +16 -0
  31. package/lib/archive/db-ops/_shared/retry-setting.js +18 -0
  32. package/lib/archive/db-ops/_shared/safe-parse-json.d.ts +11 -0
  33. package/lib/archive/db-ops/_shared/safe-parse-json.js +18 -0
  34. package/lib/archive/db-ops/_shared/types.d.ts +53 -0
  35. package/lib/archive/db-ops/_shared/types.js +1 -0
  36. package/lib/archive/db-ops/_shared/upsert-blob-ref.d.ts +25 -0
  37. package/lib/archive/db-ops/_shared/upsert-blob-ref.js +48 -0
  38. package/lib/archive/db-ops/_shared/upsert-content-type-ref.d.ts +30 -0
  39. package/lib/archive/db-ops/_shared/upsert-content-type-ref.js +45 -0
  40. package/lib/archive/db-ops/_shared/upsert-json-ref.d.ts +22 -0
  41. package/lib/archive/db-ops/_shared/upsert-json-ref.js +41 -0
  42. package/lib/archive/db-ops/_shared/upsert-response-headers.d.ts +35 -0
  43. package/lib/archive/db-ops/_shared/upsert-response-headers.js +49 -0
  44. package/lib/archive/db-ops/_shared/upsert-url-ref.d.ts +39 -0
  45. package/lib/archive/db-ops/_shared/upsert-url-ref.js +62 -0
  46. package/lib/archive/db-ops/analysis/replace-analysis-violations.d.ts +28 -0
  47. package/lib/archive/db-ops/analysis/replace-analysis-violations.js +152 -0
  48. package/lib/archive/db-ops/anchors/get-anchors-on-page.d.ts +10 -0
  49. package/lib/archive/db-ops/anchors/get-anchors-on-page.js +21 -0
  50. package/lib/archive/db-ops/config/get-base-url.d.ts +8 -0
  51. package/lib/archive/db-ops/config/get-base-url.js +14 -0
  52. package/lib/archive/db-ops/config/get-config.d.ts +10 -0
  53. package/lib/archive/db-ops/config/get-config.js +27 -0
  54. package/lib/archive/db-ops/config/get-name.d.ts +8 -0
  55. package/lib/archive/db-ops/config/get-name.js +14 -0
  56. package/lib/archive/db-ops/config/info-column-allowlist.d.ts +7 -0
  57. package/lib/archive/db-ops/config/info-column-allowlist.js +26 -0
  58. package/lib/archive/db-ops/config/info-json-columns.d.ts +5 -0
  59. package/lib/archive/db-ops/config/info-json-columns.js +10 -0
  60. package/lib/archive/db-ops/config/set-config.d.ts +12 -0
  61. package/lib/archive/db-ops/config/set-config.js +21 -0
  62. package/lib/archive/db-ops/config/update-config.d.ts +17 -0
  63. package/lib/archive/db-ops/config/update-config.js +36 -0
  64. package/lib/archive/db-ops/errors/insert-crawl-error.d.ts +15 -0
  65. package/lib/archive/db-ops/errors/insert-crawl-error.js +21 -0
  66. package/lib/archive/db-ops/errors/insert-page-error.d.ts +21 -0
  67. package/lib/archive/db-ops/errors/insert-page-error.js +28 -0
  68. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.d.ts +22 -0
  69. package/lib/archive/db-ops/errors/list-dns-burned-host-candidates.js +141 -0
  70. package/lib/archive/db-ops/html/get-html-of-page-by-id.d.ts +18 -0
  71. package/lib/archive/db-ops/html/get-html-of-page-by-id.js +29 -0
  72. package/lib/archive/db-ops/inventory/record-inventory-run.d.ts +21 -0
  73. package/lib/archive/db-ops/inventory/record-inventory-run.js +38 -0
  74. package/lib/archive/db-ops/lifecycle/checkpoint.d.ts +8 -0
  75. package/lib/archive/db-ops/lifecycle/checkpoint.js +9 -0
  76. package/lib/archive/db-ops/lifecycle/destroy.d.ts +6 -0
  77. package/lib/archive/db-ops/lifecycle/destroy.js +7 -0
  78. package/lib/archive/db-ops/lifecycle/init.d.ts +22 -0
  79. package/lib/archive/db-ops/lifecycle/init.js +42 -0
  80. package/lib/archive/db-ops/meta/get-jsonld-of-page.d.ts +13 -0
  81. package/lib/archive/db-ops/meta/get-jsonld-of-page.js +27 -0
  82. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +12 -0
  83. package/lib/archive/db-ops/meta/get-tags-of-page.js +28 -0
  84. package/lib/archive/db-ops/pages/order/set-url-order.d.ts +8 -0
  85. package/lib/archive/db-ops/pages/order/set-url-order.js +32 -0
  86. package/lib/archive/db-ops/pages/read/build-page-query.d.ts +18 -0
  87. package/lib/archive/db-ops/pages/read/build-page-query.js +40 -0
  88. package/lib/archive/db-ops/pages/read/get-crawling-state.d.ts +70 -0
  89. package/lib/archive/db-ops/pages/read/get-crawling-state.js +98 -0
  90. package/lib/archive/db-ops/pages/read/get-existing-page-urls.d.ts +15 -0
  91. package/lib/archive/db-ops/pages/read/get-existing-page-urls.js +30 -0
  92. package/lib/archive/db-ops/pages/read/get-page-count.d.ts +12 -0
  93. package/lib/archive/db-ops/pages/read/get-page-count.js +21 -0
  94. package/lib/archive/db-ops/pages/read/get-page-source-by-url.d.ts +24 -0
  95. package/lib/archive/db-ops/pages/read/get-page-source-by-url.js +28 -0
  96. package/lib/archive/db-ops/pages/read/get-pages-with-rels.d.ts +38 -0
  97. package/lib/archive/db-ops/pages/read/get-pages-with-rels.js +107 -0
  98. package/lib/archive/db-ops/pages/read/get-pages.d.ts +11 -0
  99. package/lib/archive/db-ops/pages/read/get-pages.js +51 -0
  100. package/lib/archive/db-ops/pages/read/get-scraped-html-page-count.d.ts +18 -0
  101. package/lib/archive/db-ops/pages/read/get-scraped-html-page-count.js +25 -0
  102. package/lib/archive/db-ops/pages/read/reconstruct-page-rows.d.ts +31 -0
  103. package/lib/archive/db-ops/pages/read/reconstruct-page-rows.js +32 -0
  104. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +24 -0
  105. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +93 -0
  106. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +47 -0
  107. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +124 -0
  108. package/lib/archive/db-ops/pages/write/insert-inventory-seeds.d.ts +37 -0
  109. package/lib/archive/db-ops/pages/write/insert-inventory-seeds.js +72 -0
  110. package/lib/archive/db-ops/pages/write/insert-jsonld.d.ts +17 -0
  111. package/lib/archive/db-ops/pages/write/insert-jsonld.js +49 -0
  112. package/lib/archive/db-ops/pages/write/insert-page.d.ts +36 -0
  113. package/lib/archive/db-ops/pages/write/insert-page.js +208 -0
  114. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +16 -0
  115. package/lib/archive/db-ops/pages/write/insert-tags.js +34 -0
  116. package/lib/archive/db-ops/pages/write/link-redirect-sources.d.ts +36 -0
  117. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +93 -0
  118. package/lib/archive/db-ops/pages/write/record-redirect.d.ts +35 -0
  119. package/lib/archive/db-ops/pages/write/record-redirect.js +100 -0
  120. package/lib/archive/db-ops/pages/write/set-skipped-page.d.ts +13 -0
  121. package/lib/archive/db-ops/pages/write/set-skipped-page.js +22 -0
  122. package/lib/archive/db-ops/pages/write/update-page.d.ts +29 -0
  123. package/lib/archive/db-ops/pages/write/update-page.js +334 -0
  124. package/lib/archive/db-ops/pages/write/write-page-html-blob.d.ts +19 -0
  125. package/lib/archive/db-ops/pages/write/write-page-html-blob.js +41 -0
  126. package/lib/archive/db-ops/referrers/get-redirects-for-pages.d.ts +9 -0
  127. package/lib/archive/db-ops/referrers/get-redirects-for-pages.js +15 -0
  128. package/lib/archive/db-ops/referrers/get-referrers-of-page.d.ts +17 -0
  129. package/lib/archive/db-ops/referrers/get-referrers-of-page.js +32 -0
  130. package/lib/archive/db-ops/referrers/get-referrers-of-resource.d.ts +8 -0
  131. package/lib/archive/db-ops/referrers/get-referrers-of-resource.js +15 -0
  132. package/lib/archive/db-ops/resources/build-resource-query.d.ts +25 -0
  133. package/lib/archive/db-ops/resources/build-resource-query.js +29 -0
  134. package/lib/archive/db-ops/resources/get-existing-resource-urls.d.ts +9 -0
  135. package/lib/archive/db-ops/resources/get-existing-resource-urls.js +24 -0
  136. package/lib/archive/db-ops/resources/get-resource-by-url.d.ts +13 -0
  137. package/lib/archive/db-ops/resources/get-resource-by-url.js +22 -0
  138. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +9 -0
  139. package/lib/archive/db-ops/resources/get-resource-url-list.js +13 -0
  140. package/lib/archive/db-ops/resources/get-resources.d.ts +8 -0
  141. package/lib/archive/db-ops/resources/get-resources.js +11 -0
  142. package/lib/archive/db-ops/resources/insert-inventory-resources.d.ts +24 -0
  143. package/lib/archive/db-ops/resources/insert-inventory-resources.js +64 -0
  144. package/lib/archive/db-ops/resources/insert-resource-referrers.d.ts +15 -0
  145. package/lib/archive/db-ops/resources/insert-resource-referrers.js +54 -0
  146. package/lib/archive/db-ops/resources/insert-resource.d.ts +34 -0
  147. package/lib/archive/db-ops/resources/insert-resource.js +73 -0
  148. package/lib/archive/db-ops/resources/reconstruct-resource-rows.d.ts +26 -0
  149. package/lib/archive/db-ops/resources/reconstruct-resource-rows.js +30 -0
  150. package/lib/archive/decode-html-blob.d.ts +18 -0
  151. package/lib/archive/decode-html-blob.js +31 -0
  152. package/lib/archive/derive-lineage-from-parent.d.ts +1 -1
  153. package/lib/archive/derive-lineage-from-parent.js +1 -1
  154. package/lib/archive/drop-legacy-tables.d.ts +45 -0
  155. package/lib/archive/drop-legacy-tables.js +56 -0
  156. package/lib/archive/filesystem/rename.js +1 -1
  157. package/lib/archive/get-failed-page-messages.d.ts +5 -4
  158. package/lib/archive/get-failed-page-messages.js +5 -4
  159. package/lib/archive/init-schema.d.ts +35 -39
  160. package/lib/archive/init-schema.js +99 -460
  161. package/lib/archive/limited-page-ids.d.ts +2 -1
  162. package/lib/archive/limited-page-ids.js +5 -4
  163. package/lib/archive/meta/assert-compatible-version.d.ts +24 -3
  164. package/lib/archive/meta/assert-compatible-version.js +24 -3
  165. package/lib/archive/meta/types.d.ts +87 -1
  166. package/lib/archive/meta/types.js +34 -2
  167. package/lib/archive/migrate-entity-tables.d.ts +45 -0
  168. package/lib/archive/migrate-entity-tables.js +56 -0
  169. package/lib/archive/migrate-ref-tables.d.ts +25 -0
  170. package/lib/archive/migrate-ref-tables.js +38 -0
  171. package/lib/archive/page-meta-column-maps.d.ts +32 -0
  172. package/lib/archive/page-meta-column-maps.js +43 -0
  173. package/lib/archive/page.d.ts +6 -6
  174. package/lib/archive/page.js +5 -5
  175. package/lib/archive/peek-archive-lock.d.ts +2 -2
  176. package/lib/archive/peek-archive-lock.js +2 -2
  177. package/lib/archive/populate-entity-tables/collapse-anchor-rows.d.ts +41 -0
  178. package/lib/archive/populate-entity-tables/collapse-anchor-rows.js +87 -0
  179. package/lib/archive/populate-entity-tables/derive-dom-path.d.ts +35 -0
  180. package/lib/archive/populate-entity-tables/derive-dom-path.js +72 -0
  181. package/lib/archive/populate-entity-tables/is-blob-ref-value.d.ts +16 -0
  182. package/lib/archive/populate-entity-tables/is-blob-ref-value.js +19 -0
  183. package/lib/archive/populate-entity-tables/match-images-to-dom-paths.d.ts +66 -0
  184. package/lib/archive/populate-entity-tables/match-images-to-dom-paths.js +96 -0
  185. package/lib/archive/populate-entity-tables/populate-anchor-edges.d.ts +33 -0
  186. package/lib/archive/populate-entity-tables/populate-anchor-edges.js +153 -0
  187. package/lib/archive/populate-entity-tables/populate-content-items.d.ts +40 -0
  188. package/lib/archive/populate-entity-tables/populate-content-items.js +141 -0
  189. package/lib/archive/populate-entity-tables/populate-entities.d.ts +81 -0
  190. package/lib/archive/populate-entity-tables/populate-entities.js +111 -0
  191. package/lib/archive/populate-entity-tables/populate-image-items.d.ts +91 -0
  192. package/lib/archive/populate-entity-tables/populate-image-items.js +223 -0
  193. package/lib/archive/populate-entity-tables/populate-page-meta.d.ts +33 -0
  194. package/lib/archive/populate-entity-tables/populate-page-meta.js +267 -0
  195. package/lib/archive/populate-entity-tables/populate-resource-items.d.ts +22 -0
  196. package/lib/archive/populate-entity-tables/populate-resource-items.js +114 -0
  197. package/lib/archive/populate-entity-tables/populate-resource-ref-edges.d.ts +31 -0
  198. package/lib/archive/populate-entity-tables/populate-resource-ref-edges.js +33 -0
  199. package/lib/archive/populate-entity-tables/resolve-blob-refs.d.ts +31 -0
  200. package/lib/archive/populate-entity-tables/resolve-blob-refs.js +100 -0
  201. package/lib/archive/populate-entity-tables/resolve-content-type-refs.d.ts +22 -0
  202. package/lib/archive/populate-entity-tables/resolve-content-type-refs.js +27 -0
  203. package/lib/archive/populate-entity-tables/resolve-header-sets.d.ts +49 -0
  204. package/lib/archive/populate-entity-tables/resolve-header-sets.js +122 -0
  205. package/lib/archive/populate-entity-tables/resolve-json-refs.d.ts +25 -0
  206. package/lib/archive/populate-entity-tables/resolve-json-refs.js +67 -0
  207. package/lib/archive/populate-entity-tables/resolve-text-refs.d.ts +30 -0
  208. package/lib/archive/populate-entity-tables/resolve-text-refs.js +61 -0
  209. package/lib/archive/populate-entity-tables/resolve-url-or-blob-from-maps.d.ts +21 -0
  210. package/lib/archive/populate-entity-tables/resolve-url-or-blob-from-maps.js +27 -0
  211. package/lib/archive/populate-entity-tables/resolve-url-refs.d.ts +33 -0
  212. package/lib/archive/populate-entity-tables/resolve-url-refs.js +60 -0
  213. package/lib/archive/populate-entity-tables/test-utils/count-rows.d.ts +17 -0
  214. package/lib/archive/populate-entity-tables/test-utils/count-rows.js +20 -0
  215. package/lib/archive/populate-entity-tables/test-utils/seed-content-items.d.ts +25 -0
  216. package/lib/archive/populate-entity-tables/test-utils/seed-content-items.js +42 -0
  217. package/lib/archive/populate-entity-tables/test-utils/setup-entities-db.d.ts +23 -0
  218. package/lib/archive/populate-entity-tables/test-utils/setup-entities-db.js +178 -0
  219. package/lib/archive/populate-entity-tables/types.d.ts +157 -0
  220. package/lib/archive/populate-entity-tables/types.js +12 -0
  221. package/lib/archive/populate-entity-tables/upsert-text-refs.d.ts +38 -0
  222. package/lib/archive/populate-entity-tables/upsert-text-refs.js +78 -0
  223. package/lib/archive/populate-ref-tables/classify-content-type.d.ts +16 -0
  224. package/lib/archive/populate-ref-tables/classify-content-type.js +52 -0
  225. package/lib/archive/populate-ref-tables/compute-content-hash.d.ts +22 -0
  226. package/lib/archive/populate-ref-tables/compute-content-hash.js +26 -0
  227. package/lib/archive/populate-ref-tables/compute-header-flags.d.ts +16 -0
  228. package/lib/archive/populate-ref-tables/compute-header-flags.js +70 -0
  229. package/lib/archive/populate-ref-tables/content-type-rules.d.ts +38 -0
  230. package/lib/archive/populate-ref-tables/content-type-rules.js +133 -0
  231. package/lib/archive/populate-ref-tables/create-header-table-caches.d.ts +25 -0
  232. package/lib/archive/populate-ref-tables/create-header-table-caches.js +49 -0
  233. package/lib/archive/populate-ref-tables/data-uri-url-refs-limit.d.ts +15 -0
  234. package/lib/archive/populate-ref-tables/data-uri-url-refs-limit.js +15 -0
  235. package/lib/archive/populate-ref-tables/decode-data-uri.d.ts +21 -0
  236. package/lib/archive/populate-ref-tables/decode-data-uri.js +126 -0
  237. package/lib/archive/populate-ref-tables/decompose-header-set.d.ts +29 -0
  238. package/lib/archive/populate-ref-tables/decompose-header-set.js +157 -0
  239. package/lib/archive/populate-ref-tables/decompose-url.d.ts +25 -0
  240. package/lib/archive/populate-ref-tables/decompose-url.js +70 -0
  241. package/lib/archive/populate-ref-tables/header-stability.d.ts +19 -0
  242. package/lib/archive/populate-ref-tables/header-stability.js +22 -0
  243. package/lib/archive/populate-ref-tables/header-value-cache-key.d.ts +17 -0
  244. package/lib/archive/populate-ref-tables/header-value-cache-key.js +19 -0
  245. package/lib/archive/populate-ref-tables/normalize-mime.d.ts +24 -0
  246. package/lib/archive/populate-ref-tables/normalize-mime.js +36 -0
  247. package/lib/archive/populate-ref-tables/populate-blob-refs.d.ts +38 -0
  248. package/lib/archive/populate-ref-tables/populate-blob-refs.js +134 -0
  249. package/lib/archive/populate-ref-tables/populate-content-type-refs.d.ts +27 -0
  250. package/lib/archive/populate-ref-tables/populate-content-type-refs.js +70 -0
  251. package/lib/archive/populate-ref-tables/populate-header-tables.d.ts +35 -0
  252. package/lib/archive/populate-ref-tables/populate-header-tables.js +80 -0
  253. package/lib/archive/populate-ref-tables/populate-json-refs.d.ts +29 -0
  254. package/lib/archive/populate-ref-tables/populate-json-refs.js +101 -0
  255. package/lib/archive/populate-ref-tables/populate-refs.d.ts +51 -0
  256. package/lib/archive/populate-ref-tables/populate-refs.js +62 -0
  257. package/lib/archive/populate-ref-tables/populate-text-refs.d.ts +32 -0
  258. package/lib/archive/populate-ref-tables/populate-text-refs.js +133 -0
  259. package/lib/archive/populate-ref-tables/populate-url-refs.d.ts +28 -0
  260. package/lib/archive/populate-ref-tables/populate-url-refs.js +148 -0
  261. package/lib/archive/populate-ref-tables/test-utils/count-rows.d.ts +15 -0
  262. package/lib/archive/populate-ref-tables/test-utils/count-rows.js +17 -0
  263. package/lib/archive/populate-ref-tables/types.d.ts +197 -0
  264. package/lib/archive/populate-ref-tables/types.js +7 -0
  265. package/lib/archive/populate-ref-tables/upsert-one-header-set.d.ts +34 -0
  266. package/lib/archive/populate-ref-tables/upsert-one-header-set.js +208 -0
  267. package/lib/archive/populate-ref-tables/volatile-header-names.d.ts +20 -0
  268. package/lib/archive/populate-ref-tables/volatile-header-names.js +33 -0
  269. package/lib/archive/redirect-table.d.ts +4 -2
  270. package/lib/archive/redirect-table.js +15 -10
  271. package/lib/archive/resolve-redirect-chain.d.ts +3 -3
  272. package/lib/archive/resolve-redirect-chain.js +2 -2
  273. package/lib/archive/resource.d.ts +1 -1
  274. package/lib/archive/retarget-legacy-fk-tables.d.ts +47 -0
  275. package/lib/archive/retarget-legacy-fk-tables.js +107 -0
  276. package/lib/archive/test-utils/fk-parent-tables.d.ts +15 -0
  277. package/lib/archive/test-utils/fk-parent-tables.js +19 -0
  278. package/lib/archive/test-utils/seed-content-item.d.ts +35 -0
  279. package/lib/archive/test-utils/seed-content-item.js +42 -0
  280. package/lib/archive/test-utils/setup-legacy-fk-db.d.ts +33 -0
  281. package/lib/archive/test-utils/setup-legacy-fk-db.js +270 -0
  282. package/lib/archive/types.d.ts +127 -24
  283. package/lib/archive/verify-migration/capture-rejection.d.ts +24 -0
  284. package/lib/archive/verify-migration/capture-rejection.js +31 -0
  285. package/lib/archive/verify-migration/check-anchor-edges-count.d.ts +34 -0
  286. package/lib/archive/verify-migration/check-anchor-edges-count.js +72 -0
  287. package/lib/archive/verify-migration/check-anchor-edges-sum.d.ts +13 -0
  288. package/lib/archive/verify-migration/check-anchor-edges-sum.js +27 -0
  289. package/lib/archive/verify-migration/check-content-items-count.d.ts +16 -0
  290. package/lib/archive/verify-migration/check-content-items-count.js +30 -0
  291. package/lib/archive/verify-migration/check-content-type-preservation.d.ts +22 -0
  292. package/lib/archive/verify-migration/check-content-type-preservation.js +40 -0
  293. package/lib/archive/verify-migration/check-foreign-key-integrity.d.ts +31 -0
  294. package/lib/archive/verify-migration/check-foreign-key-integrity.js +47 -0
  295. package/lib/archive/verify-migration/check-image-items-count.d.ts +12 -0
  296. package/lib/archive/verify-migration/check-image-items-count.js +26 -0
  297. package/lib/archive/verify-migration/check-page-meta-count.d.ts +15 -0
  298. package/lib/archive/verify-migration/check-page-meta-count.js +31 -0
  299. package/lib/archive/verify-migration/check-reader-parity.d.ts +23 -0
  300. package/lib/archive/verify-migration/check-reader-parity.js +211 -0
  301. package/lib/archive/verify-migration/check-resource-items-count.d.ts +17 -0
  302. package/lib/archive/verify-migration/check-resource-items-count.js +33 -0
  303. package/lib/archive/verify-migration/check-url-round-trip.d.ts +43 -0
  304. package/lib/archive/verify-migration/check-url-round-trip.js +112 -0
  305. package/lib/archive/verify-migration/types.d.ts +70 -0
  306. package/lib/archive/verify-migration/types.js +63 -0
  307. package/lib/archive/verify-migration/verify-migration.d.ts +41 -0
  308. package/lib/archive/verify-migration/verify-migration.js +120 -0
  309. package/lib/crawler/build-redirect-event.d.ts +1 -1
  310. package/lib/crawler/build-redirect-event.js +1 -1
  311. package/lib/crawler/capture-image-dom-paths.d.ts +33 -0
  312. package/lib/crawler/capture-image-dom-paths.js +39 -0
  313. package/lib/crawler/clear-dns-burned-host-cache.d.ts +1 -1
  314. package/lib/crawler/clear-dns-burned-host-cache.js +1 -1
  315. package/lib/crawler/collect-image-dom-paths.d.ts +23 -0
  316. package/lib/crawler/collect-image-dom-paths.js +64 -0
  317. package/lib/crawler/crawler.d.ts +19 -0
  318. package/lib/crawler/crawler.js +40 -26
  319. package/lib/crawler/dns-burned-host-cache.d.ts +3 -3
  320. package/lib/crawler/dns-burned-host-cache.js +3 -3
  321. package/lib/crawler/dns-burned-host-short-circuit-counter.d.ts +2 -2
  322. package/lib/crawler/dns-burned-host-short-circuit-counter.js +2 -2
  323. package/lib/crawler/inject-scope-auth.d.ts +1 -1
  324. package/lib/crawler/inject-scope-auth.js +1 -1
  325. package/lib/crawler/normalize-content-type.d.ts +1 -1
  326. package/lib/crawler/normalize-content-type.js +1 -1
  327. package/lib/crawler/types.d.ts +3 -3
  328. package/lib/crawler-orchestrator.d.ts +9 -0
  329. package/lib/crawler-orchestrator.js +44 -28
  330. package/lib/crawler.d.ts +12 -0
  331. package/lib/crawler.js +21 -0
  332. package/lib/permanent-error-kinds.d.ts +1 -1
  333. package/lib/permanent-error-kinds.js +1 -1
  334. package/lib/types.d.ts +1 -1
  335. package/lib/utils/compute-file-sha256.d.ts +5 -4
  336. package/lib/utils/compute-file-sha256.js +5 -4
  337. package/lib/utils/error/emit-error-with-retry.d.ts +1 -1
  338. package/lib/utils/error/emit-error-with-retry.js +1 -1
  339. package/package.json +10 -10
  340. package/lib/archive/migrate-crawl-errors.d.ts +0 -20
  341. package/lib/archive/migrate-crawl-errors.js +0 -38
  342. package/lib/archive/migrate-html-blob-tables.d.ts +0 -24
  343. package/lib/archive/migrate-html-blob-tables.js +0 -53
  344. package/lib/archive/migrate-inventory-runs.d.ts +0 -29
  345. package/lib/archive/migrate-inventory-runs.js +0 -52
  346. package/lib/archive/migrate-page-errors.d.ts +0 -16
  347. package/lib/archive/migrate-page-errors.js +0 -35
  348. package/lib/archive/migrate-pages-resources-source.d.ts +0 -16
  349. package/lib/archive/migrate-pages-resources-source.js +0 -46
@@ -1,3 +1,6 @@
1
+ import { createAdjunctTables } from './create-adjunct-tables.js';
2
+ import { createEntityTables } from './create-entity-tables.js';
3
+ import { createRefTables } from './create-ref-tables.js';
1
4
  /**
2
5
  * Applies the connection-level PRAGMAs that govern foreign-key enforcement
3
6
  * and BLOB-read performance. These are **per-connection** settings (libsql
@@ -34,470 +37,106 @@ export async function applyConnectionPragmas(instance) {
34
37
  /**
35
38
  * Initializes the archive database schema if tables do not exist.
36
39
  *
37
- * Schema notes:
40
+ * The schema is composed of four groups, each owned by a dedicated DDL
41
+ * function so the migration script (`scripts/migrate-to-0.13.mjs`) can
42
+ * provision the exact same shapes on archives it upgrades:
38
43
  *
39
- * - **Meta columns (v2)**: pages carries ~47 flat columns derived from
40
- * beholder 3.0.0's nested Meta shape (`canonical`, `og_*`, `twitter_*`,
41
- * `robots_*`, document basics, editorial fields) plus a `meta_extras`
42
- * JSON column for everything not flattened. URL-shaped columns are
43
- * absolutised against the page URL before write (see
44
- * `archive/meta/derive-flat-from-meta.ts`).
45
- * - **Denormalised aggregates** (`tag_count`, `jsonld_count`,
46
- * `tags_providers_csv`): computed at write time from `meta.tags` /
47
- * `meta.jsonLd` to avoid N+1 GROUP BY at Sheets-render / page-detail time.
48
- * Plan: "ファイルサイズが多少増えてもいいから取り出しパフォーマンスを優先".
49
- * - **Per-page timestamps** (`firstCrawledAt`, `lastCrawledAt`): UNIX ms.
50
- * Written by `#insertPage` on INSERT (`first = last = now`) and UPDATE
51
- * (`last = now`, `first` preserved). `resetFailedPages` deliberately
52
- * leaves them alone so failure-reset does not erase the last-success
53
- * record.
54
- * - **`page_tags`** (Wappalyzer): per-provider × external-id row shape, plus
55
- * `categories`/`sources` JSON columns. Compound indexes
56
- * `(provider, externalId)` / `(provider, pageId)` are pre-built for the
57
- * Phase 2+ "find duplicate IDs across pages" and "list pages using
58
- * provider X" hot paths — Phase 1 read perf > storage cost trade-off.
59
- * - **`page_jsonld`** (JSON-LD / SpeculationRules): one row per
60
- * `<script type="application/ld+json">` or `<script type="speculationrules">`.
61
- * `raw` is stored uncompressed (SQLite overflow pages handle large rows);
62
- * if cross-archive bulk export becomes a use case, add a `codec` column
63
- * à la `page_html_blobs`. Compound `(type, pageId)` accelerates streaming
64
- * `list_pages_by_jsonld_type` JOINs.
65
- * - **HTML snapshots** (`page_html_blobs` + `page_html_ref`): unchanged
66
- * from v1. zstd-compressed BLOBs keyed by SHA-256 for content-addressable
67
- * dedup. WITHOUT ROWID via raw SQL because knex's schema builder cannot
68
- * express it.
69
- * - **PRAGMA `page_size` and `journal_mode`** are set BEFORE any
70
- * `CREATE TABLE` because SQLite only honors `page_size` changes against
71
- * an empty database, and `journal_mode = WAL` is persistent. Other
72
- * per-connection PRAGMAs live in {@link applyConnectionPragmas}.
44
+ * - **`info`** (inline below): single-row crawl configuration.
45
+ * - **Ref / header dictionary tables** ({@link createRefTables}):
46
+ * `url_refs`, `content_type_refs`, `text_refs`, `json_refs`,
47
+ * `blob_refs`, `header_name_refs`, `header_value_refs`, `header_sets`,
48
+ * `header_set_entries`, `header_flags`. See that file for column-level
49
+ * rationale and the reason `blob_refs` uses a regular rowid PK instead
50
+ * of WITHOUT ROWID.
51
+ * - **Entity / edge tables** ({@link createEntityTables}):
52
+ * `content_items`, `page_meta`, `resource_items`, `anchor_edges`,
53
+ * `resource_ref_edges`, `image_items` — the write-model the crawler
54
+ * writes during a crawl and every reader queries. Must run AFTER
55
+ * `createRefTables` because most entity tables reference ref-table PKs.
56
+ * - **Adjunct tables** ({@link createAdjunctTables}): `page_errors`,
57
+ * `crawl_errors`, `page_tags`, `page_jsonld`, `inventory_runs`,
58
+ * `analysis_text_refs` + `analysis_violations`, `page_html_blobs` +
59
+ * `page_html_ref`. Must run AFTER `createEntityTables` because the
60
+ * page-scoped tables FK into `content_items(id)`.
73
61
  *
74
- * Pre-0.10 0.10 migration is intentionally absent. `assertCompatibleVersion`
75
- * (called before `initSchema`) rejects pre-0.10 archives with a friendly
76
- * error pointing the operator at `scripts/migrate-to-0.10.mjs`; `v0.x`
77
- * policy allows breaking changes.
62
+ * The legacy flat write-model tables (`pages` / `anchors` / `images` /
63
+ * `resources` / `resources-referrers`) are deliberately NOT created:
64
+ * they only exist inside pre-0.13 archives, where they serve as the
65
+ * populate source for `scripts/migrate-to-0.13.mjs` before that script
66
+ * drops them.
67
+ *
68
+ * **PRAGMA `page_size` and `journal_mode`** are set BEFORE any
69
+ * `CREATE TABLE` because SQLite only honors `page_size` changes against
70
+ * an empty database, and `journal_mode = WAL` is persistent. Other
71
+ * per-connection PRAGMAs live in {@link applyConnectionPragmas}.
72
+ *
73
+ * Pre-0.13 migration is intentionally absent. `assertCompatibleVersion`
74
+ * (called before `initSchema`) rejects older archives with a friendly
75
+ * error naming the migration scripts to run; `v0.x` policy allows
76
+ * breaking changes.
78
77
  * @param instance - The Knex query builder instance connected to the database.
79
78
  */
80
79
  export async function initSchema(instance) {
81
- const isExists = await instance.schema.hasTable('info');
82
- if (isExists) {
83
- return;
80
+ // Only the one-shot work (PRAGMAs + `info` creation) is gated on the
81
+ // `info` table's existence. The three DDL groups below run on EVERY
82
+ // call: each is internally idempotent (sentinel / IF NOT EXISTS /
83
+ // per-table guards), and re-running them self-heals an archive whose
84
+ // provisioning crashed partway through — `info` created but a later
85
+ // group missing. With a single all-or-nothing gate, such a stub would
86
+ // short-circuit here forever and every subsequent write would die with
87
+ // `no such table` and no way back.
88
+ if (!(await instance.schema.hasTable('info'))) {
89
+ // Page size must be set on an empty database file; once any data is
90
+ // written, only VACUUM can change it. journal_mode is also one-shot
91
+ // (persistent) and so stays here.
92
+ await instance.raw('PRAGMA page_size = 16384');
93
+ await instance.raw('PRAGMA journal_mode = WAL');
94
+ await instance.schema.createTable('info', (t) => {
95
+ t.increments('id');
96
+ t.string('version');
97
+ t.string('name');
98
+ t.string('baseUrl');
99
+ t.json('roots');
100
+ t.boolean('recursive');
101
+ t.integer('interval');
102
+ t.boolean('image');
103
+ t.boolean('fetchExternal');
104
+ t.integer('parallels');
105
+ t.json('excludes');
106
+ t.json('excludeKeywords');
107
+ t.json('excludeUrls');
108
+ t.integer('maxExcludedDepth');
109
+ t.integer('retry');
110
+ t.boolean('fromList');
111
+ t.boolean('disableQueries');
112
+ t.string('userAgent');
113
+ t.boolean('ignoreRobots');
114
+ });
84
115
  }
85
- // Page size must be set on an empty database file; once any data is
86
- // written, only VACUUM can change it. journal_mode is also one-shot
87
- // (persistent) and so stays here.
88
- await instance.raw('PRAGMA page_size = 16384');
89
- await instance.raw('PRAGMA journal_mode = WAL');
90
- await instance.schema
91
- .createTable('info', (t) => {
92
- t.increments('id');
93
- t.string('version');
94
- t.string('name');
95
- t.string('baseUrl');
96
- t.json('roots');
97
- t.boolean('recursive');
98
- t.integer('interval');
99
- t.boolean('image');
100
- t.boolean('fetchExternal');
101
- t.integer('parallels');
102
- t.json('excludes');
103
- t.json('excludeKeywords');
104
- t.json('excludeUrls');
105
- t.integer('maxExcludedDepth');
106
- t.integer('retry');
107
- t.boolean('fromList');
108
- t.boolean('disableQueries');
109
- t.string('userAgent');
110
- t.boolean('ignoreRobots');
111
- })
112
- .createTable('pages', (t) => {
113
- t.increments('id');
114
- t.string('url', 8190).notNullable().unique();
115
- t.integer('redirectDestId').unsigned().references('pages.id').defaultTo(null);
116
- t.boolean('scraped').notNullable();
117
- t.boolean('isTarget').notNullable();
118
- t.boolean('isExternal');
119
- t.integer('status');
120
- t.string('statusText');
121
- t.string('contentType').nullable();
122
- t.integer('contentLength').unsigned().nullable();
123
- t.json('responseHeaders').nullable();
124
- // Document basics
125
- t.string('lang');
126
- t.string('dir');
127
- t.string('charset');
128
- t.string('baseHref');
129
- t.text('viewport_raw');
130
- t.string('themeColor');
131
- t.string('applicationName');
132
- t.string('author');
133
- t.string('generator');
134
- t.string('publisher');
135
- // Title / description / keywords (top-level Meta fields)
136
- t.string('title');
137
- t.text('description');
138
- t.text('keywords');
139
- // Robots
140
- t.text('robots_raw');
141
- t.integer('robots_noindex');
142
- t.integer('robots_nofollow');
143
- t.integer('robots_noarchive');
144
- t.integer('robots_noimageindex');
145
- t.string('googlebot');
146
- // Link (1:1 only — array shapes live in meta_extras)
147
- t.string('canonical', 8190);
148
- t.string('amphtml', 8190);
149
- t.string('manifest', 8190);
150
- t.string('icon_href', 8190);
151
- t.string('appleTouchIcon_href', 8190);
152
- // Open Graph
153
- t.string('og_type');
154
- t.string('og_title');
155
- t.string('og_url', 8190);
156
- t.string('og_site_name');
157
- t.text('og_description');
158
- t.string('og_image', 8190);
159
- t.string('og_image_alt');
160
- t.string('og_image_width');
161
- t.string('og_image_height');
162
- t.string('og_locale');
163
- t.string('og_article_published_time');
164
- t.string('og_article_modified_time');
165
- // Twitter
166
- t.string('twitter_card');
167
- t.string('twitter_site');
168
- t.string('twitter_creator');
169
- t.string('twitter_title');
170
- t.text('twitter_description');
171
- t.string('twitter_image', 8190);
172
- // One-offs
173
- t.string('fb_app_id');
174
- t.string('verification_google');
175
- t.integer('formatDetection_telephone');
176
- // Within-archive observation timestamps (UNIX ms)
177
- t.integer('firstCrawledAt');
178
- t.integer('lastCrawledAt');
179
- // Denormalised aggregates (written at scrape time, see
180
- // archive/meta/compute-page-denormalized.ts)
181
- t.integer('tag_count');
182
- t.integer('jsonld_count');
183
- t.text('tags_providers_csv');
184
- // Catch-all JSON for nested Meta sub-objects not flattened above
185
- t.json('meta_extras');
186
- // Crawl lifecycle
187
- t.boolean('isSkipped');
188
- t.string('skipReason');
189
- t.integer('order').unsigned().nullable();
190
- // Provenance: which channel inserted this row. Values:
191
- // 'crawled' — discovered via the recursive crawl from one of `info.roots`
192
- // 'inventory-seed' — supplied directly by `crawl --inventory` URL list
193
- // 'inventory-discovered' — found by following links from an `inventory-seed` page
194
- // Used by `listIsolatedPages` only for badge display; isolation
195
- // itself is judged by `anchors.hrefId IS NULL`, not by source.
196
- t.string('source').notNullable().defaultTo('crawled');
197
- t.index('isExternal');
198
- t.index('contentType');
199
- t.index('scraped');
200
- t.index('redirectDestId');
201
- t.index('order');
202
- // Phase 1: noindex filter (list_pages) and og:type filter
203
- // (analytics) are the only new flat-column filters with enough
204
- // selectivity to benefit from an index. `lang` has cardinality 1
205
- // on mono-language sites (D-Zero's typical customer) so it is
206
- // skipped.
207
- t.index('robots_noindex');
208
- t.index('og_type');
209
- t.index('source');
210
- })
211
- .createTable('anchors', (t) => {
212
- t.increments('id');
213
- t.integer('pageId').notNullable().unsigned().references('pages.id');
214
- t.integer('hrefId').notNullable().unsigned().references('pages.id');
215
- t.string('hash');
216
- t.string('textContent').nullable();
217
- t.index('pageId');
218
- t.index('hrefId');
219
- })
220
- .createTable('images', (t) => {
221
- t.increments('id');
222
- t.integer('pageId').notNullable().unsigned().references('pages.id');
223
- t.string('src', 8190);
224
- t.string('currentSrc', 8190);
225
- t.string('alt');
226
- t.float('width').unsigned().notNullable();
227
- t.float('height').unsigned().notNullable();
228
- t.integer('naturalWidth').unsigned().notNullable();
229
- t.integer('naturalHeight').unsigned().notNullable();
230
- t.boolean('isLazy');
231
- t.integer('viewportWidth').unsigned().notNullable();
232
- t.string('sourceCode');
233
- t.index('pageId');
234
- })
235
- .createTable('resources', (t) => {
236
- t.increments('id');
237
- t.string('url', 8190).notNullable().unique();
238
- t.boolean('isExternal');
239
- t.integer('status');
240
- t.string('statusText');
241
- t.string('contentType').nullable();
242
- t.integer('contentLength').unsigned().nullable();
243
- t.string('compress').nullable();
244
- t.string('cdn').nullable();
245
- t.json('responseHeaders').nullable();
246
- // See `pages.source` for the provenance taxonomy. `inventory-seed`
247
- // rows here come from non-HTML URLs handed in by
248
- // `crawl --inventory`; `inventory-discovered` rows are sub-resources
249
- // pulled in while puppeteer rendered an inventory-seed page.
250
- t.string('source').notNullable().defaultTo('crawled');
251
- t.index('source');
252
- })
253
- .createTable('resources-referrers', (t) => {
254
- t.increments('id');
255
- t.integer('resourceId').notNullable().unsigned().references('resources.id');
256
- t.integer('pageId').notNullable().unsigned().references('pages.id');
257
- t.unique(['resourceId', 'pageId']);
258
- t.index('resourceId');
259
- t.index('pageId');
260
- })
261
- .createTable('page_errors', (t) => {
262
- // Records partial scrape failures (e.g. a viewport switch that
263
- // detaches the frame and trips beholder's @retryable into the
264
- // `retryExhausted` phase). A page can have zero or more rows here
265
- // in addition to its normal `pages` entry — the page itself is
266
- // considered successfully scraped, but image capture or another
267
- // secondary step failed for at least one device preset.
268
- t.increments('id');
269
- t.integer('pageId').notNullable().unsigned().references('pages.id');
270
- t.string('phase').notNullable();
271
- t.text('message').notNullable();
272
- t.integer('createdAt').notNullable();
273
- t.index('pageId');
274
- })
275
- .createTable('crawl_errors', (t) => {
276
- // Structured form of the crawler-level `error` channel that otherwise
277
- // only lands in `error.log`. Unlike `page_errors` these are not tied to
278
- // a scraped page (the URL may be an external link that failed DNS, or
279
- // null for a process-level error), so there is no `pageId` FK and `url`
280
- // is nullable. The cause is NOT stored — it is classified on read from
281
- // `message` so older archives (which only have `error.log`) classify the
282
- // same way.
283
- t.increments('id');
284
- t.string('url', 8190).nullable();
285
- t.boolean('isExternal');
286
- t.text('message').notNullable();
287
- t.integer('createdAt').notNullable();
288
- })
289
- .createTable('page_tags', (t) => {
290
- // Wappalyzer-derived technology detection. One row per
291
- // (provider × externalId) tuple per page. `category` is the first
292
- // element of `categories`; the full list lives in the JSON
293
- // `categories` column. `sources` records where the provider was
294
- // detected (script-src / inline / iframe-src / window-global / …).
295
- t.increments('id');
296
- t.integer('pageId')
297
- .notNullable()
298
- .unsigned()
299
- .references('pages.id')
300
- .onDelete('CASCADE');
301
- t.string('provider').notNullable();
302
- t.string('category');
303
- t.string('externalId');
304
- t.string('version');
305
- t.integer('confidence');
306
- t.json('categories');
307
- t.json('sources');
308
- t.index('pageId');
309
- t.index('provider');
310
- t.index('externalId');
311
- })
312
- .createTable('page_jsonld', (t) => {
313
- // JSON-LD and SpeculationRules entries captured from
314
- // `<script type="application/ld+json">` and
315
- // `<script type="speculationrules">`. `kind` discriminates; `type`
316
- // is the top-level `@type` extracted by classify-jsonld-type for
317
- // indexable filtering. `raw` is stored uncompressed; SQLite
318
- // overflow pages handle multi-KB JSON bodies transparently.
319
- t.increments('id');
320
- t.integer('pageId')
321
- .notNullable()
322
- .unsigned()
323
- .references('pages.id')
324
- .onDelete('CASCADE');
325
- t.string('kind').notNullable();
326
- t.string('type');
327
- t.text('raw').notNullable();
328
- t.json('parsed');
329
- t.text('parseError');
330
- t.index('pageId');
331
- t.index('type');
332
- })
333
- .createTable('inventory_runs', (t) => {
334
- // One row per successful `--inventory <list>` invocation. The
335
- // archive's audit log of "when did we apply which deploy list
336
- // at what scale". `.bak` is removed on success so this table
337
- // is the only durable provenance record. Schema rationale +
338
- // non-goals live in {@link migrateInventoryRuns}.
339
- t.increments('id');
340
- t.string('ran_at').notNullable();
341
- t.string('list_label').nullable();
342
- t.string('source_file_sha256', 64).nullable();
343
- t.integer('total_lines').nullable();
344
- t.integer('new_pages').nullable();
345
- t.integer('new_resources').nullable();
346
- t.integer('scope_skipped').nullable();
347
- t.text('notes').nullable();
348
- t.index('ran_at');
349
- });
350
- // ON DELETE CASCADE and compound indexes for the new tables. Knex's
351
- // schema builder can't express CASCADE / compound indexes inline in a
352
- // way that round-trips through libsql consistently, so we use raw SQL
353
- // to mirror the `page_html_ref` pattern.
354
- await instance.raw('CREATE INDEX page_tags_provider_extId ON page_tags(provider, externalId)');
355
- await instance.raw('CREATE INDEX page_tags_provider_pageId ON page_tags(provider, pageId)');
356
- await instance.raw('CREATE INDEX page_jsonld_type_pageId ON page_jsonld(type, pageId)');
357
- // Content-addressable HTML blob storage. Knex's schema builder doesn't
358
- // expose a WITHOUT ROWID toggle, so the BLOB tables are created via raw
359
- // SQL. WITHOUT ROWID keeps the rows packed inside the b-tree leaves
360
- // (no hidden rowid + secondary index pair), which matters for the blob
361
- // table where a 32-byte hash PK + multi-KB body is the dominant row
362
- // shape.
363
- await instance.raw(`
364
- CREATE TABLE page_html_blobs (
365
- hash BLOB PRIMARY KEY,
366
- body BLOB NOT NULL,
367
- codec TEXT NOT NULL CHECK(codec IN ('zstd', 'none')),
368
- size_raw INTEGER NOT NULL,
369
- size_stored INTEGER NOT NULL
370
- ) WITHOUT ROWID
371
- `);
372
- await instance.raw(`
373
- CREATE TABLE page_html_ref (
374
- page_id INTEGER PRIMARY KEY REFERENCES pages(id) ON DELETE CASCADE,
375
- hash BLOB NOT NULL REFERENCES page_html_blobs(hash)
376
- ) WITHOUT ROWID
377
- `);
378
- await instance.raw('CREATE INDEX idx_page_html_ref_hash ON page_html_ref(hash)');
379
- // Composite covering index for the default Pages-view filter + url-ordered
380
- // scan. Without it, `listPages` on a 400k-row archive runs ~15s per page
381
- // click (SCAN pages USING pages_scraped_index + TEMP B-TREE FOR ORDER BY);
382
- // with it, the same query runs ~45ms (368x speedup, confirmed via
383
- // `scripts/bench-partial-listfilter.mjs` against a real customer archive).
384
- // The same index also serves `listIsolatedPages`, `listIsolatedClusters`,
385
- // and `getSummary`'s HTML-page counts.
386
- //
387
- // **Column order: `(isExternal, scraped, redirectDestId, url, contentType)`.**
388
- // The leading `isExternal` is critical: the Pages view's default
389
- // "external excluded" filter adds `WHERE isExternal = 0` to both the
390
- // SELECT and the paginate-query COUNT. A previous version of this index
391
- // (`(scraped, redirectDestId, url, contentType)`) shipped without
392
- // `isExternal`, and the SELECT picked it up (`ORDER BY url` forced the
393
- // match) while the COUNT — having no `ORDER BY` — fell back to the
394
- // single-column `pages_isexternal_index` + scan + per-row WHERE filter,
395
- // costing ~8.7s for the COUNT alone on a 165k-internal-page archive.
396
- // Putting `isExternal` first makes both shapes pick this index as a
397
- // covering scan (~33ms COUNT, ~1ms SELECT warm).
398
- //
399
- // **DO NOT RUN `ANALYZE` ON .nitpicker ARCHIVES.** With ANALYZE statistics
400
- // available, the planner switches the JOIN paths in `listLinks`,
401
- // `getLinkGraph`, and `listPageLinks` to use this index for source/dest
402
- // seeks (SCAN dest → SEARCH anchors → SEARCH source) instead of the
403
- // existing `SCAN anchors → rowid seek` plan. That regression takes those
404
- // queries from ~15s to ~500s (33x worse). The unanalyzed-table heuristic
405
- // happens to pick the right plan for the joins while still picking the new
406
- // index for `listPages` because the column order exactly matches the
407
- // WHERE+ORDER predicates. If a future change adds `ANALYZE` anywhere in
408
- // the crawler / viewer / MCP / migration paths, this index must be
409
- // re-evaluated first.
410
- await instance.raw('CREATE INDEX idx_pages_listfilter ON pages(isExternal, scraped, redirectDestId, url, contentType)');
411
- // Covering index for `listUnusedResources`. Without it the query SCAN s
412
- // `resources_url_unique` (every resource, including externals) then
413
- // filters `isExternal = 0` row-by-row — ~66s on the bench archive. With
414
- // the `(isExternal, url)` leading prefix, the planner serves the WHERE
415
- // + ORDER BY url from one covering scan — ~7.5s (8.8x). Same
416
- // no-ANALYZE invariant applies (see `idx_pages_listfilter` above);
417
- // validated against the 4 regression sentinels in
418
- // `scripts/bench-unused-images.mjs`.
419
- await instance.raw('CREATE INDEX idx_resources_internal_url ON resources(isExternal, url)');
420
- // Covering index for `listImages`. The default query joins `images` to
421
- // `pages` and orders by `pages.url`. Without this index the planner
422
- // scans `images` first, seeks `pages` by rowid, and pays a TEMP B-TREE
423
- // FOR ORDER BY (~32s on the bench archive). With the index the plan
424
- // flips to SCAN pages (via `pages_url_unique`, url-ordered already)
425
- // → SEARCH images via the covering pageId index — no temp sort, ~16s
426
- // (2.0x). The included columns (src, alt, dimensions, isLazy) make
427
- // `idx_images_covering` covering for every `select` `listImages` does,
428
- // so the SEARCH does not need to materialise the underlying row.
429
- // Validated by `scripts/bench-unused-images.mjs`.
430
- await instance.raw('CREATE INDEX idx_images_covering ON images(pageId, src, alt, width, height, naturalWidth, naturalHeight, isLazy)');
431
- // Targets `getSummary` Q2 (metadata fulfilment) + Q3 (content-type
432
- // histogram). With this index Q2 and Q3 both become covering
433
- // (`SEARCH ... USING COVERING INDEX`) — the SELECT columns are
434
- // contained inside the index entry, so no rowid lookup is needed.
435
- // Q1 (status histogram) also picks this index for its seek but
436
- // keeps `USE TEMP B-TREE FOR GROUP BY` because the index column
437
- // order leads with `contentType, isExternal` while Q1's GROUP BY
438
- // is `(isExternal, status)` — the residual ordering inside the
439
- // `scraped=1 AND redirectDestId IS NULL` slice does not match.
440
- // Empirically this still gives the largest net win because Q2 +
441
- // Q3 dominate `getSummary` on archives whose `pages` table dwarfs
442
- // the SQLite page cache (10 GB bench: 1157 ms → 717 ms, 38 %).
443
- //
444
- // **An additional candidate index `(scraped, redirectDestId,
445
- // isExternal, status)` was empirically rejected**: in isolation it
446
- // matches Q1's GROUP BY column order and would eliminate the temp
447
- // B-tree there, but in combination with this one or with the
448
- // `pages_scraped_index` fallback the planner shifted to plans that
449
- // regressed `getSummary` to 4.6-10 s (PR #96 教訓 — bulk index
450
- // addition without ANALYZE confuses the heuristic). The
451
- // `idx_pages_summary_contenttype` form below is the only summary
452
- // index that survived the matrix test in
453
- // `scripts/bench-summary-configs.mjs`.
454
- //
455
- // Column order rationale:
456
- //
457
- // 1. `scraped` — leading seek key. All summary queries constrain
458
- // it to `=1`.
459
- // 2. `redirectDestId` — post-seek filter, IS NULL folded into the
460
- // seek key by SQLite's index walk without needing ANALYZE
461
- // (per operator forum; the IS NULL leading column rule only
462
- // bites when the column is the LEADING one and there is no
463
- // other equality constraint).
464
- // 3. `contentType` — the column Q3 groups by.
465
- // 4. `isExternal` — Q3's second GROUP BY column AND Q2's WHERE
466
- // constraint (`isExternal=0`).
467
- // 5. `isSkipped` — Q1/Q3's residual `(isSkipped=0 OR IS NULL)`
468
- // filter (`excludeSkippedPages`). Having it in the index lets
469
- // the residual filter use the index entry instead of a per-row
470
- // rowid lookup.
471
- //
472
- // `id` is implicitly included (every SQLite index entry carries
473
- // the rowid), so the `count(id)` aggregates cover off-index.
474
- //
475
- // **No-ANALYZE invariant** identical to `idx_pages_listfilter`
476
- // (PR #96): the column order matches the WHERE+GROUP BY predicates
477
- // exactly, so SQLite's heuristic-only planner picks it without
478
- // needing `sqlite_stat1`. Adding `ANALYZE` would risk planner
479
- // shifts in this and other queries.
480
- //
481
- // **Regression check**: `listPages` / `listPages COUNT` /
482
- // `listLinks broken` / `listPageLinks` plans were re-verified
483
- // against this index — `idx_pages_listfilter` continues to win
484
- // for all of them. See `scripts/bench-summary-configs.mjs`.
485
- await instance.raw('CREATE INDEX idx_pages_summary_contenttype ON pages(scraped, redirectDestId, contentType, isExternal, isSkipped)');
486
- // Targets `getSummary` Q4 (`failedPageIdRows`) — selects pages with
487
- // `scraped=1 AND status=-1 AND redirectDestId IS NULL`. `status=-1`
488
- // is highly selective (a few hundred rows on archives with
489
- // ~400 k `scraped=1` pages), but without this index the planner
490
- // seeks all `scraped=1` rows via `pages_scraped_index` and then
491
- // row-by-row filters status, costing ~5 s. The 3-column form
492
- // `(scraped, status, redirectDestId)` is fully covering for
493
- // `SELECT id` and gives a 5113 ms → 14 ms (~365x) reduction
494
- // verified by `scripts/prototype-summary-indexes.mjs`.
495
- //
496
- // Note the column order: `status` comes second so the `(scraped=1
497
- // AND status=-1)` 2-column equality seek lands directly in the
498
- // failed-page slice without scanning the 400 k+ healthy rows.
499
- // Putting `redirectDestId` last keeps it as a 3rd-level seek
500
- // constraint that the planner folds into the slice once the
501
- // (scraped, status) pair is fixed.
502
- await instance.raw('CREATE INDEX idx_pages_summary_failed ON pages(scraped, status, redirectDestId)');
116
+ // 0.13 ref / header dictionary tables. DDL + column-level rationale
117
+ // lives in {@link createRefTables}, shared with the migration script's
118
+ // schema catch-up a divergence between the two paths would silently
119
+ // break the population step's UNIQUE / CHECK contract.
120
+ // `createRefTables` uses bare CREATE TABLE, so it needs a sentinel
121
+ // guard here (all 10 tables are created together — any one of them
122
+ // works as the sentinel).
123
+ if (!(await instance.schema.hasTable('url_refs'))) {
124
+ await createRefTables(instance);
125
+ }
126
+ // 0.13 core entity / edge tables — the write-model the crawler writes
127
+ // during a crawl and every reader queries. MUST run after
128
+ // {@link createRefTables} because `content_items`, `page_meta`,
129
+ // `resource_items`, `anchor_edges`, and `image_items` all reference
130
+ // the ref tables (`url_refs`, `content_type_refs`, `text_refs`,
131
+ // `json_refs`, `blob_refs`, `header_sets`) via FK clauses.
132
+ await createEntityTables(instance);
133
+ // Adjunct tables that FK into `content_items` (page_errors / page_tags /
134
+ // page_jsonld / analysis_* / page_html_*) plus the standalone log tables
135
+ // (crawl_errors / inventory_runs). MUST run after
136
+ // {@link createEntityTables} so the FK targets exist. DDL +
137
+ // column-level rationale lives in {@link createAdjunctTables}, which is
138
+ // shared with `scripts/migrate-to-0.13.mjs` — a divergence between the
139
+ // two paths is exactly how migrated archives ended up with stale
140
+ // `REFERENCES pages(id)` declarations in the pre-0.13 era.
141
+ await createAdjunctTables(instance);
503
142
  }
@@ -1,7 +1,8 @@
1
1
  import type { Knex } from 'knex';
2
2
  /**
3
3
  * Returns a Knex subquery builder that selects page IDs with pagination,
4
- * ordered by the `order` column (nulls last), excluding redirected pages.
4
+ * ordered by `content_items.crawl_order` (nulls last), excluding
5
+ * redirected pages.
5
6
  * @param limit - The maximum number of page IDs to return.
6
7
  * @param offset - The number of page IDs to skip before returning results.
7
8
  */
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Returns a Knex subquery builder that selects page IDs with pagination,
3
- * ordered by the `order` column (nulls last), excluding redirected pages.
3
+ * ordered by `content_items.crawl_order` (nulls last), excluding
4
+ * redirected pages.
4
5
  * @param limit - The maximum number of page IDs to return.
5
6
  * @param offset - The number of page IDs to skip before returning results.
6
7
  */
@@ -8,9 +9,9 @@ export function limitedPageIds(limit, offset) {
8
9
  return async (qb) => {
9
10
  await qb
10
11
  .select('id')
11
- .from('pages')
12
- .orderByRaw('`order` ASC NULLS LAST')
13
- .whereNull('redirectDestId')
12
+ .from('content_items')
13
+ .orderByRaw('`crawl_order` ASC NULLS LAST')
14
+ .whereNull('redirect_dest_id')
14
15
  .limit(limit)
15
16
  .offset(offset);
16
17
  };
@@ -1,18 +1,39 @@
1
1
  import type { Knex } from 'knex';
2
2
  /**
3
3
  * Minimum `info.version` this build accepts. Archives older than this must
4
- * be upgraded with `scripts/migrate-to-0.10.mjs` before they can be opened.
4
+ * be upgraded by running the matching migration script before they can be
5
+ * opened (see {@link IncompatibleArchiveError}'s message for the mapping
6
+ * from archive version to script).
7
+ *
8
+ * This value tracks the **archive format cut**, not the npm `pkg.version`:
9
+ * a format-breaking change bumps it ahead of the corresponding release,
10
+ * and releases that do not cut the format leave it unchanged. The crawler
11
+ * writes this exact value into every new archive's `info.version`.
5
12
  *
6
13
  * History:
7
14
  *
8
15
  * - **pre-0.10**: HTML snapshots in `snapshot-html.zip` (#75), then
9
16
  * relocated to `page_html_blobs` (#84); pages table has flat `noindex`,
10
17
  * `og:type`-style columns derived from beholder 2.x's flat `Meta`.
11
- * - **0.10.0**: this build. `page_html_blobs` BLOB storage (#75/#84) +
18
+ * - **0.10.0**: `page_html_blobs` BLOB storage (#75/#84) +
12
19
  * nested-`Meta`-derived flat columns, `meta_extras` JSON, `page_tags` /
13
20
  * `page_jsonld` tables, denormalised aggregates (#85).
21
+ * - **0.13.0**: this build. Write-model refactor (epic #103) —
22
+ * `content_items` / `page_meta` / `resource_items` / `anchor_edges` /
23
+ * `resource_ref_edges` / `image_items` entity tables plus `url_refs` /
24
+ * `text_refs` / `content_type_refs` / `json_refs` / `blob_refs` /
25
+ * `header_flags` ref tables. Readers query the new tables exclusively;
26
+ * pre-0.13 archives must be upgraded with `scripts/migrate-to-0.13.mjs`
27
+ * which bumps `info.version` to `0.13.0` on completion.
28
+ * @example
29
+ * import { REQUIRED_FORMAT_VERSION } from '@nitpicker/crawler';
30
+ *
31
+ * // Warn an operator before attempting to open an old archive:
32
+ * if (compareSemver(archiveInfo.version, REQUIRED_FORMAT_VERSION) < 0) {
33
+ * console.error(`Archive needs migration to format ${REQUIRED_FORMAT_VERSION}`);
34
+ * }
14
35
  */
15
- export declare const REQUIRED_FORMAT_VERSION = "0.10.0";
36
+ export declare const REQUIRED_FORMAT_VERSION = "0.13.0";
16
37
  /**
17
38
  * Verifies that the archive's on-disk format is compatible with this build.
18
39
  *