@nitpicker/crawler 0.18.1 → 0.19.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 (138) hide show
  1. package/lib/append-retry-failed-common-setup-phases.d.ts +15 -0
  2. package/lib/append-retry-failed-common-setup-phases.js +21 -0
  3. package/lib/append-setup-phases.d.ts +17 -0
  4. package/lib/append-setup-phases.js +22 -0
  5. package/lib/archive/archive-accessor.d.ts +31 -5
  6. package/lib/archive/archive-accessor.js +46 -24
  7. package/lib/archive/archive.d.ts +115 -37
  8. package/lib/archive/archive.js +118 -52
  9. package/lib/archive/cache/extract-archive-to-cache.d.ts +10 -1
  10. package/lib/archive/cache/extract-archive-to-cache.js +14 -4
  11. package/lib/archive/create-adjunct-tables.d.ts +12 -2
  12. package/lib/archive/create-adjunct-tables.js +91 -29
  13. package/lib/archive/create-entity-tables.d.ts +5 -1
  14. package/lib/archive/create-entity-tables.js +6 -1
  15. package/lib/archive/database.d.ts +39 -9
  16. package/lib/archive/database.js +63 -18
  17. package/lib/archive/db-ops/lifecycle/init.d.ts +14 -2
  18. package/lib/archive/db-ops/lifecycle/init.js +29 -11
  19. package/lib/archive/db-ops/meta/get-custom-elements-of-page.d.ts +11 -0
  20. package/lib/archive/db-ops/meta/get-custom-elements-of-page.js +15 -0
  21. package/lib/archive/db-ops/meta/get-page-technologies-of-page.d.ts +11 -0
  22. package/lib/archive/db-ops/meta/get-page-technologies-of-page.js +15 -0
  23. package/lib/archive/db-ops/meta/get-technology-signals-of-page.d.ts +11 -0
  24. package/lib/archive/db-ops/meta/get-technology-signals-of-page.js +15 -0
  25. package/lib/archive/db-ops/pages/order/set-url-order.d.ts +6 -1
  26. package/lib/archive/db-ops/pages/order/set-url-order.js +7 -1
  27. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  28. package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +6 -1
  29. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +12 -3
  30. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +6 -1
  31. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +10 -2
  32. package/lib/archive/db-ops/pages/write/insert-custom-elements.d.ts +27 -0
  33. package/lib/archive/db-ops/pages/write/insert-custom-elements.js +40 -0
  34. package/lib/archive/db-ops/pages/write/insert-page.d.ts +4 -4
  35. package/lib/archive/db-ops/pages/write/insert-page.js +3 -2
  36. package/lib/archive/db-ops/pages/write/insert-technologies.d.ts +25 -0
  37. package/lib/archive/db-ops/pages/write/insert-technologies.js +45 -0
  38. package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
  39. package/lib/archive/db-ops/pages/write/update-page.js +38 -16
  40. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
  41. package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
  42. package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
  43. package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
  44. package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
  45. package/lib/archive/filesystem/parse-pax-path.js +47 -0
  46. package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
  47. package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
  48. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
  49. package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
  50. package/lib/archive/filesystem/tar.d.ts +13 -1
  51. package/lib/archive/filesystem/tar.js +63 -7
  52. package/lib/archive/filesystem/untar.d.ts +13 -0
  53. package/lib/archive/filesystem/untar.js +68 -8
  54. package/lib/archive/init-schema.d.ts +1 -1
  55. package/lib/archive/init-schema.js +6 -4
  56. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
  57. package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
  58. package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
  59. package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
  60. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
  61. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
  62. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
  63. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
  64. package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
  65. package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
  66. package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
  67. package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
  68. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
  69. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
  70. package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
  71. package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
  72. package/lib/archive/meta/technologies/types.d.ts +68 -0
  73. package/lib/archive/meta/technologies/types.js +13 -0
  74. package/lib/archive/meta/types.d.ts +97 -30
  75. package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
  76. package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
  77. package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
  78. package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
  79. package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
  80. package/lib/archive/migrate-info-main-content-selector.js +14 -3
  81. package/lib/archive/migrate-info-roots.d.ts +6 -1
  82. package/lib/archive/migrate-info-roots.js +14 -3
  83. package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
  84. package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
  85. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
  86. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
  87. package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
  88. package/lib/archive/migrate-main-contents-columns.js +14 -3
  89. package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
  90. package/lib/archive/migrate-page-meta-body-hash.js +14 -3
  91. package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
  92. package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
  93. package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
  94. package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
  95. package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
  96. package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
  97. package/lib/archive/page.d.ts +37 -13
  98. package/lib/archive/page.js +45 -17
  99. package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
  100. package/lib/archive/retarget-legacy-fk-tables.js +61 -15
  101. package/lib/archive/types.d.ts +18 -0
  102. package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
  103. package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
  104. package/lib/crawler/capture-custom-elements.d.ts +33 -0
  105. package/lib/crawler/capture-custom-elements.js +39 -0
  106. package/lib/crawler/close-browser-safely.d.ts +3 -3
  107. package/lib/crawler/close-browser-safely.js +8 -17
  108. package/lib/crawler/collect-custom-elements.d.ts +33 -0
  109. package/lib/crawler/collect-custom-elements.js +110 -0
  110. package/lib/crawler/crawler.js +101 -46
  111. package/lib/crawler/fetch-destination.js +6 -13
  112. package/lib/crawler/find-package-dir.d.ts +24 -0
  113. package/lib/crawler/find-package-dir.js +39 -0
  114. package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
  115. package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
  116. package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
  117. package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
  118. package/lib/crawler/types.d.ts +23 -0
  119. package/lib/crawler-orchestrator.d.ts +53 -9
  120. package/lib/crawler-orchestrator.js +223 -46
  121. package/lib/crawler.d.ts +11 -1
  122. package/lib/crawler.js +9 -0
  123. package/lib/inventory-setup-phases.d.ts +32 -0
  124. package/lib/inventory-setup-phases.js +45 -0
  125. package/lib/resume-setup-phases.d.ts +16 -0
  126. package/lib/resume-setup-phases.js +24 -0
  127. package/lib/retry-failed-setup-phases.d.ts +19 -0
  128. package/lib/retry-failed-setup-phases.js +24 -0
  129. package/lib/setup-recovery-phase-labels.d.ts +21 -0
  130. package/lib/setup-recovery-phase-labels.js +24 -0
  131. package/lib/types.d.ts +145 -0
  132. package/package.json +11 -8
  133. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
  134. package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
  135. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
  136. package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
  137. package/lib/archive/meta/summarize-tags.d.ts +0 -16
  138. package/lib/archive/meta/summarize-tags.js +0 -33
package/lib/crawler.d.ts CHANGED
@@ -23,10 +23,11 @@ export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entr
23
23
  export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
24
24
  export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
25
25
  export type { ArchiveCacheEntry, ArchiveCacheEntryKind } from './archive/cache/types.js';
26
+ export { copyFileWithProgress } from './archive/filesystem/copy-file-with-progress.js';
26
27
  export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
27
28
  export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
28
29
  export type { ArchiveLockHolder } from './archive/peek-archive-lock.js';
29
- export type { FlatPageMetaColumns, JsonLdRow, JsonLdRowForInsert, TagRow, TagRowForInsert, JsonLdSummary, TagsSummary, TagInventoryEntry, PageDenormalizedColumns, } from './archive/meta/types.js';
30
+ export type { FlatPageMetaColumns, JsonLdRow, JsonLdRowForInsert, TagRow, TagRowForInsert, JsonLdSummary, PageDenormalizedColumns, TechnologySignalRow, TechnologySignalRowForInsert, PageTechnologyRow, PageTechnologyRowForInsert, } from './archive/meta/types.js';
30
31
  export { IncompatibleArchiveError } from './archive/meta/types.js';
31
32
  export { REQUIRED_FORMAT_VERSION } from './archive/meta/assert-compatible-version.js';
32
33
  export { computeBodyHash } from './archive/body-hash/compute-body-hash.js';
@@ -35,6 +36,11 @@ export { computeTierAAliasKey } from './archive/url-alias/compute-tier-a-alias-k
35
36
  export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-key.js';
36
37
  export { computeShapeKey } from './crawler/dedupe/compute-shape-key.js';
37
38
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
39
+ export { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
40
+ export { APPEND_SETUP_PHASES } from './append-setup-phases.js';
41
+ export { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
42
+ export { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
43
+ export { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
38
44
  export * from './types.js';
39
45
  export * from './crawler/types.js';
40
46
  export { classifyErrorKind } from './classify-error-kind.js';
@@ -48,7 +54,11 @@ export { probeNetwork } from './crawler/probe-network.js';
48
54
  export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
49
55
  export { chooseProbeHost } from './crawler/choose-probe-host.js';
50
56
  export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
57
+ export { assertPuppeteerSharedWithBeholder } from './crawler/assert-puppeteer-shared-with-beholder.js';
51
58
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
59
+ export { scanJsResourceForLicenseComment } from './crawler/scan-js-resource-for-license-comment.js';
60
+ export type { ScanJsResourcesForTechnologySignalsOptions, ScanJsResourcesForTechnologySignalsResult, } from './crawler/scan-js-resources-for-technology-signals.js';
61
+ export { scanJsResourcesForTechnologySignals } from './crawler/scan-js-resources-for-technology-signals.js';
52
62
  export { populateEntityTables } from './archive/populate-entity-tables/populate-entities.js';
53
63
  export type { PageDomPathResolver } from './archive/populate-entity-tables/populate-image-items.js';
54
64
  export { populateRefTables } from './archive/populate-ref-tables/populate-refs.js';
package/lib/crawler.js CHANGED
@@ -23,6 +23,7 @@ export { resolveArchiveCacheDir } from './archive/cache/resolve-archive-cache-di
23
23
  export { listArchiveCacheEntries } from './archive/cache/list-archive-cache-entries.js';
24
24
  export { clearArchiveCacheRoot } from './archive/cache/clear-archive-cache-root.js';
25
25
  export { clearArchiveCacheEntry } from './archive/cache/clear-archive-cache-entry.js';
26
+ export { copyFileWithProgress } from './archive/filesystem/copy-file-with-progress.js';
26
27
  export { acquireArchiveLock, ArchiveLockError } from './archive/archive-lock.js';
27
28
  export { peekArchiveLockHolder } from './archive/peek-archive-lock.js';
28
29
  export { IncompatibleArchiveError } from './archive/meta/types.js';
@@ -34,6 +35,11 @@ export { computeTierBAliasKey } from './archive/url-alias/compute-tier-b-alias-k
34
35
  export { computeShapeKey } from './crawler/dedupe/compute-shape-key.js';
35
36
  // Core
36
37
  export { DEFAULT_EXCLUDED_EXTERNAL_URLS, CrawlerOrchestrator, } from './crawler-orchestrator.js';
38
+ export { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
39
+ export { APPEND_SETUP_PHASES } from './append-setup-phases.js';
40
+ export { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
41
+ export { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
42
+ export { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
37
43
  export * from './types.js';
38
44
  export * from './crawler/types.js';
39
45
  export { classifyErrorKind } from './classify-error-kind.js';
@@ -45,7 +51,10 @@ export { probeNetwork } from './crawler/probe-network.js';
45
51
  export { computeOutageClampTimestamp } from './archive/db-ops/outages/compute-outage-clamp-timestamp.js';
46
52
  export { chooseProbeHost } from './crawler/choose-probe-host.js';
47
53
  export { assertChromeIsInstalled } from './crawler/assert-chrome-installed.js';
54
+ export { assertPuppeteerSharedWithBeholder } from './crawler/assert-puppeteer-shared-with-beholder.js';
48
55
  export { computeFileSha256 } from './utils/compute-file-sha256.js';
56
+ export { scanJsResourceForLicenseComment } from './crawler/scan-js-resource-for-license-comment.js';
57
+ export { scanJsResourcesForTechnologySignals } from './crawler/scan-js-resources-for-technology-signals.js';
49
58
  // 0.13 ref-table population (issue #191, epic #103). Exposed as the
50
59
  // public seam that the migration script (`scripts/migrate-to-0.13.mjs`)
51
60
  // drives against an already-connected archive.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels
3
+ * `CrawlerOrchestrator.inventory` announces, in call order, before its
4
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
5
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
6
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
7
+ * entry — duplicating this sequence by hand at the CLI layer would silently
8
+ * drift the moment a phase is added, renamed, or reordered here.
9
+ *
10
+ * This is the **superset** sequence — `inventory` has three
11
+ * `initializedCallback` call sites, each reached after a different prefix of
12
+ * this list depending on the input:
13
+ * - no novel URLs (everything already known / out of scope): stops after
14
+ * index 3 (`'Checking for already-known URLs'`), skips the `.bak` backup
15
+ * entirely
16
+ * - novel URLs are all non-HTML: stops after index 7
17
+ * (`'Recording excluded pages'`) — nothing to render, no crawl state to
18
+ * rebuild
19
+ * - at least one novel HTML seed: runs the full 12-phase sequence
20
+ *
21
+ * The CLI's setup task list pre-builds all 12 rows and marks whichever
22
+ * suffix never got announced as skipped once `initializedCallback` fires —
23
+ * it does not need to know in advance which of the three prefixes a given
24
+ * run will take. Note `'Loading crawl state'` appears twice (indices 2 and
25
+ * 8): the first read is scoped to the pre-ingestion pending-URL warning, the
26
+ * second re-reads state after the new inventory seeds have been inserted.
27
+ *
28
+ * On failure, a `SETUP_RECOVERY_PHASE_LABELS` entry
29
+ * (`'Restoring archive from backup'` or `'Persisting ingested inventory
30
+ * state'`) is announced instead of continuing this sequence.
31
+ */
32
+ export declare const INVENTORY_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Loading crawl state", "Checking for already-known URLs", "Backing up archive", "Recording non-HTML resources", "Recording HTML seed pages", "Recording excluded pages", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels
3
+ * `CrawlerOrchestrator.inventory` announces, in call order, before its
4
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
5
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
6
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
7
+ * entry — duplicating this sequence by hand at the CLI layer would silently
8
+ * drift the moment a phase is added, renamed, or reordered here.
9
+ *
10
+ * This is the **superset** sequence — `inventory` has three
11
+ * `initializedCallback` call sites, each reached after a different prefix of
12
+ * this list depending on the input:
13
+ * - no novel URLs (everything already known / out of scope): stops after
14
+ * index 3 (`'Checking for already-known URLs'`), skips the `.bak` backup
15
+ * entirely
16
+ * - novel URLs are all non-HTML: stops after index 7
17
+ * (`'Recording excluded pages'`) — nothing to render, no crawl state to
18
+ * rebuild
19
+ * - at least one novel HTML seed: runs the full 12-phase sequence
20
+ *
21
+ * The CLI's setup task list pre-builds all 12 rows and marks whichever
22
+ * suffix never got announced as skipped once `initializedCallback` fires —
23
+ * it does not need to know in advance which of the three prefixes a given
24
+ * run will take. Note `'Loading crawl state'` appears twice (indices 2 and
25
+ * 8): the first read is scoped to the pre-ingestion pending-URL warning, the
26
+ * second re-reads state after the new inventory seeds have been inserted.
27
+ *
28
+ * On failure, a `SETUP_RECOVERY_PHASE_LABELS` entry
29
+ * (`'Restoring archive from backup'` or `'Persisting ingested inventory
30
+ * state'`) is announced instead of continuing this sequence.
31
+ */
32
+ export const INVENTORY_SETUP_PHASES = [
33
+ 'Extracting archive',
34
+ 'Loading archive config',
35
+ 'Loading crawl state',
36
+ 'Checking for already-known URLs',
37
+ 'Backing up archive',
38
+ 'Recording non-HTML resources',
39
+ 'Recording HTML seed pages',
40
+ 'Recording excluded pages',
41
+ 'Loading crawl state',
42
+ 'Loading resource list',
43
+ 'Loading scraped page count',
44
+ 'Restoring crawl state',
45
+ ];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels `CrawlerOrchestrator.resume`
3
+ * announces, in call order, before its `initializedCallback` fires. Single
4
+ * source of truth for both the `onPhase` call sites in
5
+ * `crawler-orchestrator.ts` and the CLI's setup task list (`create-setup-task-list.ts`),
6
+ * which pre-builds one task-list row per entry — duplicating this sequence by
7
+ * hand at the CLI layer would silently drift the moment a phase is added,
8
+ * renamed, or reordered here.
9
+ *
10
+ * `resume` never takes a `.bak` backup (there is nothing to restore — the
11
+ * interrupted crawl's tmp dir is the source of truth), so unlike
12
+ * `append`/`inventory`/`retryFailed` this sequence has no failure-only
13
+ * recovery phase and no branching: every `resume` call announces exactly
14
+ * these seven phases in this order.
15
+ */
16
+ export declare const RESUME_SETUP_PHASES: readonly ["Reconnecting to archive", "Loading archive config", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels `CrawlerOrchestrator.resume`
3
+ * announces, in call order, before its `initializedCallback` fires. Single
4
+ * source of truth for both the `onPhase` call sites in
5
+ * `crawler-orchestrator.ts` and the CLI's setup task list (`create-setup-task-list.ts`),
6
+ * which pre-builds one task-list row per entry — duplicating this sequence by
7
+ * hand at the CLI layer would silently drift the moment a phase is added,
8
+ * renamed, or reordered here.
9
+ *
10
+ * `resume` never takes a `.bak` backup (there is nothing to restore — the
11
+ * interrupted crawl's tmp dir is the source of truth), so unlike
12
+ * `append`/`inventory`/`retryFailed` this sequence has no failure-only
13
+ * recovery phase and no branching: every `resume` call announces exactly
14
+ * these seven phases in this order.
15
+ */
16
+ export const RESUME_SETUP_PHASES = [
17
+ 'Reconnecting to archive',
18
+ 'Loading archive config',
19
+ 'Loading dedupe-cap shape keys',
20
+ 'Loading crawl state',
21
+ 'Loading resource list',
22
+ 'Loading scraped page count',
23
+ 'Restoring crawl state',
24
+ ];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Ordered `SetupProgressCallbacks.onPhase` labels
3
+ * `CrawlerOrchestrator.retryFailed` announces, in call order, before its
4
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
5
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
6
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
7
+ * entry — duplicating this sequence by hand at the CLI layer would silently
8
+ * drift the moment a phase is added, renamed, or reordered here. Shares its
9
+ * prefix/suffix with `APPEND_SETUP_PHASES` via
10
+ * `APPEND_RETRY_FAILED_COMMON_SETUP_PHASES` — `'Resetting failed pages'` is
11
+ * the one step unique to `retryFailed()`.
12
+ *
13
+ * Linear on the success path (no branching before `initializedCallback`). On
14
+ * failure — anywhere from `archive.resetFailedPages` through the crawl
15
+ * itself — a `SETUP_RECOVERY_PHASE_LABELS` entry
16
+ * (`'Restoring archive from backup'`) is announced instead of continuing
17
+ * this sequence.
18
+ */
19
+ export declare const RETRY_FAILED_SETUP_PHASES: readonly ["Extracting archive", "Loading archive config", "Backing up archive", "Resetting failed pages", "Loading dedupe-cap shape keys", "Loading crawl state", "Loading resource list", "Loading scraped page count", "Restoring crawl state"];
@@ -0,0 +1,24 @@
1
+ import { APPEND_RETRY_FAILED_COMMON_SETUP_PHASES } from './append-retry-failed-common-setup-phases.js';
2
+ /**
3
+ * Ordered `SetupProgressCallbacks.onPhase` labels
4
+ * `CrawlerOrchestrator.retryFailed` announces, in call order, before its
5
+ * `initializedCallback` fires. Single source of truth for both the `onPhase`
6
+ * call sites in `crawler-orchestrator.ts` and the CLI's setup task list
7
+ * (`create-setup-task-list.ts`), which pre-builds one task-list row per
8
+ * entry — duplicating this sequence by hand at the CLI layer would silently
9
+ * drift the moment a phase is added, renamed, or reordered here. Shares its
10
+ * prefix/suffix with `APPEND_SETUP_PHASES` via
11
+ * `APPEND_RETRY_FAILED_COMMON_SETUP_PHASES` — `'Resetting failed pages'` is
12
+ * the one step unique to `retryFailed()`.
13
+ *
14
+ * Linear on the success path (no branching before `initializedCallback`). On
15
+ * failure — anywhere from `archive.resetFailedPages` through the crawl
16
+ * itself — a `SETUP_RECOVERY_PHASE_LABELS` entry
17
+ * (`'Restoring archive from backup'`) is announced instead of continuing
18
+ * this sequence.
19
+ */
20
+ export const RETRY_FAILED_SETUP_PHASES = [
21
+ ...APPEND_RETRY_FAILED_COMMON_SETUP_PHASES.prefix,
22
+ 'Resetting failed pages',
23
+ ...APPEND_RETRY_FAILED_COMMON_SETUP_PHASES.suffix,
24
+ ];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `SetupProgressCallbacks.onPhase` labels announced only from a failure
3
+ * path — never part of the ordered success-path sequences
4
+ * (`RESUME_SETUP_PHASES` / `APPEND_SETUP_PHASES` / `RETRY_FAILED_SETUP_PHASES` /
5
+ * `INVENTORY_SETUP_PHASES`) those static methods otherwise announce in order.
6
+ *
7
+ * The CLI's setup task list treats any `onPhase` label in this set as
8
+ * unplanned: instead of advancing to the next pre-built row, it inserts a
9
+ * new row for it (`ctx.insertNext`) so the recovery copy's byte progress
10
+ * stays visible, rather than trying to guess in advance where in the
11
+ * success-path sequence a failure might interrupt it.
12
+ *
13
+ * - `'Restoring archive from backup'` — `append` / `inventory` /
14
+ * `retryFailed` all take this path when anything from the post-backup
15
+ * setup step through the crawl itself throws.
16
+ * - `'Persisting ingested inventory state'` — `inventory`-only: once its
17
+ * pre-insert + audit-row write has committed, a later failure (state
18
+ * rebuild or the crawl itself) must persist the ingested rows to disk
19
+ * instead of restoring the pre-inventory `.bak`, or they would be lost.
20
+ */
21
+ export declare const SETUP_RECOVERY_PHASE_LABELS: readonly ["Restoring archive from backup", "Persisting ingested inventory state"];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `SetupProgressCallbacks.onPhase` labels announced only from a failure
3
+ * path — never part of the ordered success-path sequences
4
+ * (`RESUME_SETUP_PHASES` / `APPEND_SETUP_PHASES` / `RETRY_FAILED_SETUP_PHASES` /
5
+ * `INVENTORY_SETUP_PHASES`) those static methods otherwise announce in order.
6
+ *
7
+ * The CLI's setup task list treats any `onPhase` label in this set as
8
+ * unplanned: instead of advancing to the next pre-built row, it inserts a
9
+ * new row for it (`ctx.insertNext`) so the recovery copy's byte progress
10
+ * stays visible, rather than trying to guess in advance where in the
11
+ * success-path sequence a failure might interrupt it.
12
+ *
13
+ * - `'Restoring archive from backup'` — `append` / `inventory` /
14
+ * `retryFailed` all take this path when anything from the post-backup
15
+ * setup step through the crawl itself throws.
16
+ * - `'Persisting ingested inventory state'` — `inventory`-only: once its
17
+ * pre-insert + audit-row write has committed, a later failure (state
18
+ * rebuild or the crawl itself) must persist the ingested rows to disk
19
+ * instead of restoring the pre-inventory `.bak`, or they would be lost.
20
+ */
21
+ export const SETUP_RECOVERY_PHASE_LABELS = [
22
+ 'Restoring archive from backup',
23
+ 'Persisting ingested inventory state',
24
+ ];
package/lib/types.d.ts CHANGED
@@ -1,4 +1,75 @@
1
+ import type { APPEND_SETUP_PHASES } from './append-setup-phases.js';
2
+ import type { INVENTORY_SETUP_PHASES } from './inventory-setup-phases.js';
3
+ import type { RESUME_SETUP_PHASES } from './resume-setup-phases.js';
4
+ import type { RETRY_FAILED_SETUP_PHASES } from './retry-failed-setup-phases.js';
5
+ import type { SETUP_RECOVERY_PHASE_LABELS } from './setup-recovery-phase-labels.js';
1
6
  import type { CrawlerError, PageData } from './utils/types/types.js';
7
+ /**
8
+ * Every label `SetupProgressCallbacks.onPhase` can be called with, across
9
+ * all four `CrawlerOrchestrator` setup sequences plus the failure-only
10
+ * recovery phases. See `RESUME_SETUP_PHASES` / `APPEND_SETUP_PHASES` /
11
+ * `RETRY_FAILED_SETUP_PHASES` / `INVENTORY_SETUP_PHASES` /
12
+ * `SETUP_RECOVERY_PHASE_LABELS` for what each label means and when it fires.
13
+ */
14
+ export type SetupPhaseLabel = (typeof RESUME_SETUP_PHASES)[number] | (typeof APPEND_SETUP_PHASES)[number] | (typeof RETRY_FAILED_SETUP_PHASES)[number] | (typeof INVENTORY_SETUP_PHASES)[number] | (typeof SETUP_RECOVERY_PHASE_LABELS)[number];
15
+ /**
16
+ * Progress callbacks for the setup phase of `CrawlerOrchestrator.append` /
17
+ * `inventory` / `retryFailed` / `resume` (issue #294) — everything from
18
+ * `Archive.open`'s untar through `Crawler#resume`'s in-memory state rebuild,
19
+ * which all runs **before** `initializedCallback` fires (before the CLI's
20
+ * event-based progress display — `attachCrawlDisplay` — has anything to
21
+ * subscribe to). A large archive's setup can itself take tens of seconds to
22
+ * minutes (untar, `.bak` copy, chunked page/resource re-scans), and without
23
+ * this it looked completely silent — including before the CLI's own
24
+ * "🐳 archive (...)" header line, which normally establishes that the
25
+ * process is even alive.
26
+ *
27
+ * Passed as a plain callback object (not routed through the orchestrator's
28
+ * event emitter) because the orchestrator instance the emitter lives on
29
+ * does not exist yet for most of this phase — `Archive.open`, the `.bak`
30
+ * copy, and the pre-`new CrawlerOrchestrator(...)` scope/repromote work all
31
+ * run before there is anything to attach a listener to.
32
+ */
33
+ export interface SetupProgressCallbacks {
34
+ /**
35
+ * Called once at the start of each named setup step that has no
36
+ * countable progress of its own (a single query, an in-memory rebuild).
37
+ * @param label - Human-readable description of the step starting.
38
+ */
39
+ onPhase?: (label: SetupPhaseLabel) => void;
40
+ /**
41
+ * Called during `Archive.open`'s tar extraction, with bytes read so far
42
+ * and the archive's total size.
43
+ */
44
+ onExtractProgress?: (readBytes: number, totalBytes: number) => void;
45
+ /**
46
+ * Called during a `.bak` backup or restore copy, with bytes copied so
47
+ * far and the source file's total size — the pre-mutation backup and
48
+ * the on-failure restore both go through this one callback; the
49
+ * caller's own {@link SetupProgressCallbacks.onPhase} call immediately
50
+ * before distinguishes which copy is running.
51
+ */
52
+ onCopyProgress?: (copiedBytes: number, totalBytes: number) => void;
53
+ /**
54
+ * Called for chunked/keyset-scan progress within whichever named phase
55
+ * is currently running (`repromoteExternalPages`, `getResourceUrlList`,
56
+ * `resetFailedPages`) — the unit varies by phase (pages processed vs.
57
+ * ids scanned), so callers label it using the most recent `onPhase`
58
+ * call.
59
+ * @param processed - Units completed so far, including this update.
60
+ * @param total - Total units this phase will process.
61
+ */
62
+ onChunkProgress?: (processed: number, total: number) => void;
63
+ /**
64
+ * Called instead of `console.error` for self-healing schema migration
65
+ * notices that can fire during `Archive.open`/`Archive.resume` while
66
+ * this setup phase's own `Lanes`/`TaskList` display is active (issue
67
+ * #294) — a bare `console.error` there corrupts the display's cursor
68
+ * tracking. Forwarded to {@link import('../archive/archive.js').ArchiveOpenOptions.onLog}.
69
+ * Omit to fall back to `console.error`.
70
+ */
71
+ onLog?: (message: string) => void;
72
+ }
2
73
  /**
3
74
  * Aggregate counts captured during a `--inventory` invocation, forwarded to
4
75
  * `#writeInventoryRunRow` so the audit log row is consistent between the
@@ -115,6 +186,80 @@ export interface CrawlEvent {
115
186
  /** Absolute path of the archive file that was written. */
116
187
  filePath: string;
117
188
  };
189
+ /**
190
+ * Emitted once at the start of each of `Archive.write()`'s internal
191
+ * steps (issue #294) — `checkpoint`/`remove` have no countable progress
192
+ * of their own (a single synchronous PRAGMA and a directory removal),
193
+ * so without this a large archive's write looks frozen between
194
+ * `writeFileStart` and the `writeTarProgress` byte updates.
195
+ */
196
+ writeStep: {
197
+ /** Which step of `Archive.write()` is starting. */
198
+ step: 'checkpoint' | 'rename' | 'tar' | 'remove';
199
+ };
200
+ /**
201
+ * Emitted as archive bytes are written during `Archive.write()`'s tar
202
+ * step (issue #294) — tarring a large (15 GB+) archive can take minutes,
203
+ * and without this the CLI shows nothing between `writeStep: 'tar'` and
204
+ * `writeFileEnd`.
205
+ */
206
+ writeTarProgress: {
207
+ /** Bytes written so far. */
208
+ writtenBytes: number;
209
+ /** Estimated total bytes (sum of source file sizes; tar adds headers/padding). */
210
+ totalBytes: number;
211
+ };
212
+ /**
213
+ * Emitted as pages are re-ordered after crawling completes (issue #294)
214
+ * — `setUrlOrder()` loads every internal page, sorts it in JS, then
215
+ * writes the result back in chunks, which can take seconds to minutes
216
+ * on a large archive with no other signal it hasn't hung.
217
+ */
218
+ sortingUrls: {
219
+ /** Pages assigned an order so far. */
220
+ processed: number;
221
+ /** Total internal pages being ordered. */
222
+ total: number;
223
+ };
224
+ /**
225
+ * Emitted once, only when `[Symbol.asyncDispose]`'s call to
226
+ * `Archive.close()` discovers the archive file doesn't exist on disk
227
+ * yet and falls back to writing it there (issue #294) — e.g. the
228
+ * caller's own explicit `CrawlerOrchestrator.write()` threw partway
229
+ * through (network/disk error during tar) before finishing. Emitted
230
+ * before the `writeStep`/`writeTarProgress` events this recovery write
231
+ * reuses, so a listener knows why a fresh write is starting after one
232
+ * already appeared to fail.
233
+ */
234
+ recoveringArchiveWrite: Record<string, never>;
235
+ /**
236
+ * Emitted once, right before `crawlEnd`'s final `WriteQueue.drain()`,
237
+ * when that queue still has enqueued-but-not-yet-executed writes (issue
238
+ * #294) — the deal's own per-page progress display has already stopped
239
+ * updating by this point (the crawler itself is done), so a queue still
240
+ * draining page/resource INSERTs would otherwise look like the process
241
+ * hung between the last progress line and the archive write starting.
242
+ * Not emitted when the queue is already empty — nothing to wait on, so
243
+ * nothing to explain.
244
+ */
245
+ flushingPendingWrites: {
246
+ /** Enqueued operations still waiting or executing at emission time. */
247
+ pending: number;
248
+ };
249
+ /**
250
+ * Emitted once per session-summary notice `#finalizeCrawlSession` has to
251
+ * report — the DNS-burned-host short-circuit count and/or the
252
+ * network-outage summary, each only when its count is nonzero (issue
253
+ * #294 code review). Fires in the same crawl-tail window as
254
+ * `flushingPendingWrites`/`sortingUrls` (after crawling finishes, before
255
+ * the static factory method returns), so a listener can route it the
256
+ * same way instead of a bare `console.error` corrupting whatever
257
+ * `Lanes`/`TaskList` display happens to be active at that point.
258
+ */
259
+ crawlSessionNotice: {
260
+ /** The formatted, ready-to-display notice text. */
261
+ message: string;
262
+ };
118
263
  /**
119
264
  * Emitted when an error occurs during crawling or archiving.
120
265
  */
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@nitpicker/crawler",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Web crawler engine with headless browser rendering and archive storage",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
7
+ "engines": {
8
+ "node": ">=24.11.0"
9
+ },
7
10
  "repository": {
8
11
  "type": "git",
9
12
  "url": "https://github.com/d-zero-dev/nitpicker.git",
@@ -27,17 +30,17 @@
27
30
  "clean": "tsc --build --clean"
28
31
  },
29
32
  "dependencies": {
30
- "@d-zero/beholder": "4.2.2",
31
- "@d-zero/dealer": "1.10.4",
32
- "@d-zero/fs": "0.2.6",
33
- "@d-zero/shared": "0.22.5",
33
+ "@d-zero/beholder": "4.2.3",
34
+ "@d-zero/dealer": "1.12.0",
35
+ "@d-zero/fs": "0.2.7",
36
+ "@d-zero/shared": "0.23.0",
34
37
  "ansi-colors": "4.1.3",
35
38
  "debug": "4.4.3",
36
39
  "follow-redirects": "1.16.0",
37
- "fs-extra": "11.3.6",
40
+ "fs-extra": "11.4.0",
38
41
  "knex": "3.3.0",
39
42
  "libsql": "0.5.29",
40
- "puppeteer": "25.3.0",
43
+ "puppeteer": "25.5.0",
41
44
  "robots-parser": "3.0.1",
42
45
  "tar": "7.5.22"
43
46
  },
@@ -48,5 +51,5 @@
48
51
  "@types/tar": "7.0.87",
49
52
  "@types/unzipper": "0.10.11"
50
53
  },
51
- "gitHead": "cf366bfa2ca1e0528623b3fbe1c17ac4237ca4e4"
54
+ "gitHead": "b79b5c304820d924168a139e7148e0593c29fe1c"
52
55
  }
@@ -1,12 +0,0 @@
1
- import type { TagRow } from '../../meta/types.js';
2
- import type { Knex } from 'knex';
3
- /**
4
- * Retrieves all `page_tags` rows for the given page id, parsed back into
5
- * {@link TagRow} shape (with `categories` and `sources` JSON columns
6
- * deserialised).
7
- *
8
- * Read-side counterpart to `insertTags`.
9
- * @param knex - Knex query builder connected to the archive DB.
10
- * @param pageId
11
- */
12
- export declare function getTagsOfPage(knex: Knex, pageId: number): Promise<TagRow[]>;
@@ -1,28 +0,0 @@
1
- import { safeParseJson } from '../_shared/safe-parse-json.js';
2
- /**
3
- * Retrieves all `page_tags` rows for the given page id, parsed back into
4
- * {@link TagRow} shape (with `categories` and `sources` JSON columns
5
- * deserialised).
6
- *
7
- * Read-side counterpart to `insertTags`.
8
- * @param knex - Knex query builder connected to the archive DB.
9
- * @param pageId
10
- */
11
- export async function getTagsOfPage(knex, pageId) {
12
- const rows = await knex
13
- .select('id', 'pageId', 'provider', 'category', 'externalId', 'version', 'confidence', 'categories', 'sources')
14
- .from('page_tags')
15
- .where('pageId', pageId)
16
- .orderBy('id', 'asc');
17
- return rows.map((r) => ({
18
- id: r.id,
19
- pageId: r.pageId,
20
- provider: r.provider,
21
- category: r.category,
22
- externalId: r.externalId,
23
- version: r.version,
24
- confidence: r.confidence,
25
- categories: r.categories === null ? [] : (safeParseJson(r.categories) ?? []),
26
- sources: r.sources === null ? [] : (safeParseJson(r.sources) ?? []),
27
- }));
28
- }
@@ -1,16 +0,0 @@
1
- import type { PageData } from '../../../../utils/types/types.js';
2
- import type { Knex } from 'knex';
3
- /**
4
- * Replaces the page's Wappalyzer tag rows with the freshly captured set.
5
- * Called inside `updatePage`'s transaction unconditionally — tag
6
- * detection draws on `<script src>` / `<iframe src>` / window globals /
7
- * response headers, not the HTML body, so external pages that skip
8
- * rendering still contribute tags.
9
- *
10
- * Same empty-guard as `insertJsonLd`: an empty array does not wipe
11
- * prior rows on a degraded re-scrape.
12
- * @param pageId
13
- * @param meta
14
- * @param trx
15
- */
16
- export declare function insertTags(pageId: number, meta: PageData['meta'], trx: Knex.Transaction): Promise<void>;
@@ -1,34 +0,0 @@
1
- import { eachSplitted } from '../../../../utils/array/each-splitted.js';
2
- import { extractTagsForArchive } from '../../../meta/extract-tags-for-archive.js';
3
- /**
4
- * Replaces the page's Wappalyzer tag rows with the freshly captured set.
5
- * Called inside `updatePage`'s transaction unconditionally — tag
6
- * detection draws on `<script src>` / `<iframe src>` / window globals /
7
- * response headers, not the HTML body, so external pages that skip
8
- * rendering still contribute tags.
9
- *
10
- * Same empty-guard as `insertJsonLd`: an empty array does not wipe
11
- * prior rows on a degraded re-scrape.
12
- * @param pageId
13
- * @param meta
14
- * @param trx
15
- */
16
- export async function insertTags(pageId, meta, trx) {
17
- const partial = extractTagsForArchive(meta.tags);
18
- if (partial.length === 0)
19
- return;
20
- const rows = partial.map((p) => ({
21
- pageId,
22
- provider: p.provider,
23
- category: p.category,
24
- externalId: p.externalId,
25
- version: p.version,
26
- confidence: p.confidence,
27
- categories: JSON.stringify(p.categories),
28
- sources: JSON.stringify(p.sources),
29
- }));
30
- await trx('page_tags').where('pageId', pageId).delete();
31
- await eachSplitted(rows, 100, async (chunk) => {
32
- await trx('page_tags').insert(chunk);
33
- });
34
- }
@@ -1,16 +0,0 @@
1
- import type { TagRow, TagsSummary } from './types.js';
2
- /**
3
- * Builds the {@link TagsSummary} object returned by `get-page-detail` from a
4
- * page's tag rows.
5
- *
6
- * Provides a `provider → unique IDs[]` map so consumers can answer questions
7
- * like "what GTM containers are on this page?" without fetching the full
8
- * `categories` / `sources` JSON columns.
9
- *
10
- * IDs within each provider are sorted and de-duplicated. Providers with no
11
- * `externalId` rows still appear in the map with an empty `[]` so the
12
- * caller knows the provider was detected.
13
- * @param rows - All `page_tags` rows for one page.
14
- * @returns Summary with `count` and `providerIds` map.
15
- */
16
- export declare function summarizeTags(rows: readonly TagRow[]): TagsSummary;
@@ -1,33 +0,0 @@
1
- /**
2
- * Builds the {@link TagsSummary} object returned by `get-page-detail` from a
3
- * page's tag rows.
4
- *
5
- * Provides a `provider → unique IDs[]` map so consumers can answer questions
6
- * like "what GTM containers are on this page?" without fetching the full
7
- * `categories` / `sources` JSON columns.
8
- *
9
- * IDs within each provider are sorted and de-duplicated. Providers with no
10
- * `externalId` rows still appear in the map with an empty `[]` so the
11
- * caller knows the provider was detected.
12
- * @param rows - All `page_tags` rows for one page.
13
- * @returns Summary with `count` and `providerIds` map.
14
- */
15
- export function summarizeTags(rows) {
16
- const providerIds = {};
17
- for (const row of rows) {
18
- if (!(row.provider in providerIds)) {
19
- providerIds[row.provider] = new Set();
20
- }
21
- if (row.externalId !== null) {
22
- providerIds[row.provider].add(row.externalId);
23
- }
24
- }
25
- const sorted = {};
26
- for (const provider of Object.keys(providerIds).toSorted()) {
27
- sorted[provider] = [...providerIds[provider]].toSorted();
28
- }
29
- return {
30
- count: rows.length,
31
- providerIds: sorted,
32
- };
33
- }