@nitpicker/crawler 0.18.1 → 0.20.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 (141) 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 +140 -56
  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/handle-scrape-end.js +19 -10
  115. package/lib/crawler/resolve-result-went-off-host.d.ts +34 -0
  116. package/lib/crawler/resolve-result-went-off-host.js +39 -0
  117. package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
  118. package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
  119. package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
  120. package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
  121. package/lib/crawler/types.d.ts +34 -1
  122. package/lib/crawler-orchestrator.d.ts +53 -9
  123. package/lib/crawler-orchestrator.js +223 -46
  124. package/lib/crawler.d.ts +11 -1
  125. package/lib/crawler.js +9 -0
  126. package/lib/inventory-setup-phases.d.ts +32 -0
  127. package/lib/inventory-setup-phases.js +45 -0
  128. package/lib/resume-setup-phases.d.ts +16 -0
  129. package/lib/resume-setup-phases.js +24 -0
  130. package/lib/retry-failed-setup-phases.d.ts +19 -0
  131. package/lib/retry-failed-setup-phases.js +24 -0
  132. package/lib/setup-recovery-phase-labels.d.ts +21 -0
  133. package/lib/setup-recovery-phase-labels.js +24 -0
  134. package/lib/types.d.ts +145 -0
  135. package/package.json +11 -8
  136. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
  137. package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
  138. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
  139. package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
  140. package/lib/archive/meta/summarize-tags.d.ts +0 -16
  141. package/lib/archive/meta/summarize-tags.js +0 -33
@@ -7,5 +7,10 @@ import type { Knex } from 'knex';
7
7
  * `initSchema` call — idempotent, and self-healing for archives whose
8
8
  * provisioning crashed partway through.
9
9
  * @param instance - The Knex query builder instance connected to the database.
10
+ * @param onLog - Called instead of `console.error` when this migration
11
+ * actually applies (issue #294: a bare `console.error` here can fire
12
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
13
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
14
+ * `console.error` when omitted (direct/test callers).
10
15
  */
11
- export declare function migrateInventoryRunsInvalidSkipped(instance: Knex): Promise<void>;
16
+ export declare function migrateInventoryRunsInvalidSkipped(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -6,8 +6,13 @@
6
6
  * `initSchema` call — idempotent, and self-healing for archives whose
7
7
  * provisioning crashed partway through.
8
8
  * @param instance - The Knex query builder instance connected to the database.
9
+ * @param onLog - Called instead of `console.error` when this migration
10
+ * actually applies (issue #294: a bare `console.error` here can fire
11
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
12
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
13
+ * `console.error` when omitted (direct/test callers).
9
14
  */
10
- export async function migrateInventoryRunsInvalidSkipped(instance) {
15
+ export async function migrateInventoryRunsInvalidSkipped(instance, onLog) {
11
16
  const hasTable = await instance.schema.hasTable('inventory_runs');
12
17
  if (!hasTable) {
13
18
  return;
@@ -19,6 +24,12 @@ export async function migrateInventoryRunsInvalidSkipped(instance) {
19
24
  await instance.schema.table('inventory_runs', (t) => {
20
25
  t.integer('invalid_skipped');
21
26
  });
22
- // eslint-disable-next-line no-console
23
- console.error('[migrate] inventory_runs.invalid_skipped column added');
27
+ const message = '[migrate] inventory_runs.invalid_skipped column added';
28
+ if (onLog) {
29
+ onLog(message);
30
+ }
31
+ else {
32
+ // eslint-disable-next-line no-console
33
+ console.error(message);
34
+ }
24
35
  }
@@ -17,5 +17,10 @@ import type { Knex } from 'knex';
17
17
  * itself after `assertCompatibleVersion` rejects pre-0.13 archives) the
18
18
  * table is always present.
19
19
  * @param instance - The Knex query builder instance connected to the database.
20
+ * @param onLog - Called instead of `console.error` when this migration
21
+ * actually applies (issue #294: a bare `console.error` here can fire
22
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
23
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
24
+ * `console.error` when omitted (direct/test callers).
20
25
  */
21
- export declare function migrateMainContentsColumns(instance: Knex): Promise<void>;
26
+ export declare function migrateMainContentsColumns(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -16,8 +16,13 @@
16
16
  * itself after `assertCompatibleVersion` rejects pre-0.13 archives) the
17
17
  * table is always present.
18
18
  * @param instance - The Knex query builder instance connected to the database.
19
+ * @param onLog - Called instead of `console.error` when this migration
20
+ * actually applies (issue #294: a bare `console.error` here can fire
21
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
22
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
23
+ * `console.error` when omitted (direct/test callers).
19
24
  */
20
- export async function migrateMainContentsColumns(instance) {
25
+ export async function migrateMainContentsColumns(instance, onLog) {
21
26
  const hasPageMeta = await instance.schema.hasTable('page_meta');
22
27
  if (!hasPageMeta) {
23
28
  return;
@@ -45,6 +50,12 @@ export async function migrateMainContentsColumns(instance) {
45
50
  t.integer('scroll_height_desktop');
46
51
  t.integer('scroll_height_mobile');
47
52
  });
48
- // eslint-disable-next-line no-console
49
- console.error('[migrate] page_meta.main_content_* / scroll_height_* columns added');
53
+ const message = '[migrate] page_meta.main_content_* / scroll_height_* columns added';
54
+ if (onLog) {
55
+ onLog(message);
56
+ }
57
+ else {
58
+ // eslint-disable-next-line no-console
59
+ console.error(message);
60
+ }
50
61
  }
@@ -35,5 +35,10 @@ import type { Knex } from 'knex';
35
35
  * runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
36
36
  * pre-0.13 archives) the table is always present.
37
37
  * @param instance - The Knex query builder instance connected to the database.
38
+ * @param onLog - Called instead of `console.error` when this migration
39
+ * actually applies (issue #294: a bare `console.error` here can fire
40
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
41
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
42
+ * `console.error` when omitted (direct/test callers).
38
43
  */
39
- export declare function migratePageMetaBodyHash(instance: Knex): Promise<void>;
44
+ export declare function migratePageMetaBodyHash(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -34,8 +34,13 @@
34
34
  * runs (after `initSchema`, itself after `assertCompatibleVersion` rejects
35
35
  * pre-0.13 archives) the table is always present.
36
36
  * @param instance - The Knex query builder instance connected to the database.
37
+ * @param onLog - Called instead of `console.error` when this migration
38
+ * actually applies (issue #294: a bare `console.error` here can fire
39
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
40
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
41
+ * `console.error` when omitted (direct/test callers).
37
42
  */
38
- export async function migratePageMetaBodyHash(instance) {
43
+ export async function migratePageMetaBodyHash(instance, onLog) {
39
44
  const hasPageMeta = await instance.schema.hasTable('page_meta');
40
45
  if (!hasPageMeta) {
41
46
  return;
@@ -45,8 +50,14 @@ export async function migratePageMetaBodyHash(instance) {
45
50
  await instance.schema.table('page_meta', (t) => {
46
51
  t.binary('body_hash');
47
52
  });
48
- // eslint-disable-next-line no-console
49
- console.error('[migrate] page_meta.body_hash column added');
53
+ const message = '[migrate] page_meta.body_hash column added';
54
+ if (onLog) {
55
+ onLog(message);
56
+ }
57
+ else {
58
+ // eslint-disable-next-line no-console
59
+ console.error(message);
60
+ }
50
61
  }
51
62
  await instance.raw('CREATE INDEX IF NOT EXISTS idx_page_meta_body_hash ON page_meta(body_hash)');
52
63
  }
@@ -18,5 +18,10 @@ import type { Knex } from 'knex';
18
18
  *
19
19
  * Idempotent: adding the column is a no-op once it exists.
20
20
  * @param instance - The Knex query builder instance connected to the database.
21
+ * @param onLog - Called instead of `console.error` when this migration
22
+ * actually applies (issue #294: a bare `console.error` here can fire
23
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
24
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
25
+ * `console.error` when omitted (direct/test callers).
21
26
  */
22
- export declare function migratePageMetaConsoleErrorCount(instance: Knex): Promise<void>;
27
+ export declare function migratePageMetaConsoleErrorCount(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -17,8 +17,13 @@
17
17
  *
18
18
  * Idempotent: adding the column is a no-op once it exists.
19
19
  * @param instance - The Knex query builder instance connected to the database.
20
+ * @param onLog - Called instead of `console.error` when this migration
21
+ * actually applies (issue #294: a bare `console.error` here can fire
22
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
23
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
24
+ * `console.error` when omitted (direct/test callers).
20
25
  */
21
- export async function migratePageMetaConsoleErrorCount(instance) {
26
+ export async function migratePageMetaConsoleErrorCount(instance, onLog) {
22
27
  const hasPageMeta = await instance.schema.hasTable('page_meta');
23
28
  if (!hasPageMeta) {
24
29
  return;
@@ -28,7 +33,13 @@ export async function migratePageMetaConsoleErrorCount(instance) {
28
33
  await instance.schema.table('page_meta', (t) => {
29
34
  t.integer('console_error_count');
30
35
  });
31
- // eslint-disable-next-line no-console
32
- console.error('[migrate] page_meta.console_error_count column added');
36
+ const message = '[migrate] page_meta.console_error_count column added';
37
+ if (onLog) {
38
+ onLog(message);
39
+ }
40
+ else {
41
+ // eslint-disable-next-line no-console
42
+ console.error(message);
43
+ }
33
44
  }
34
45
  }
@@ -0,0 +1,33 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Adds the `page_meta.main_content_custom_element_count` column to archives
4
+ * created before this feature.
5
+ *
6
+ * `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
7
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
8
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
9
+ * against an *existing* table — adding a column to the DDL string never
10
+ * reaches an archive whose `page_meta` predates this change. Same catch-up
11
+ * shape as {@link import('./migrate-page-meta-console-error-count.js').migratePageMetaConsoleErrorCount}.
12
+ *
13
+ * Deliberately its own file rather than an addition to
14
+ * {@link import('./migrate-main-contents-columns.js').migrateMainContentsColumns}:
15
+ * that migration early-returns once its sentinel column
16
+ * (`main_content_word_count`) already exists, so an archive that predates
17
+ * only this one column (but already has the other sixteen main-content
18
+ * columns) would never reach an appended `ALTER TABLE` inside it.
19
+ *
20
+ * No separate backfill: a page that has never been re-scraped since this
21
+ * feature shipped stays `NULL` (unknown), which is correct — it is not
22
+ * conflated with `0` (capture succeeded, zero custom elements found). See
23
+ * {@link import('./meta/compute-main-contents-denormalized.js').computeMainContentsDenormalized}.
24
+ *
25
+ * Idempotent: adding the column is a no-op once it exists.
26
+ * @param instance - The Knex query builder instance connected to the database.
27
+ * @param onLog - Called instead of `console.error` when this migration
28
+ * actually applies (issue #294: a bare `console.error` here can fire
29
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
30
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
31
+ * `console.error` when omitted (direct/test callers).
32
+ */
33
+ export declare function migratePageMetaCustomElementCount(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Adds the `page_meta.main_content_custom_element_count` column to archives
3
+ * created before this feature.
4
+ *
5
+ * `page_meta` is provisioned via a bare `CREATE TABLE IF NOT EXISTS` in
6
+ * {@link import('./create-entity-tables.js').createEntityTables}, which
7
+ * self-heals a *missing table* on every `initSchema` call but is a no-op
8
+ * against an *existing* table — adding a column to the DDL string never
9
+ * reaches an archive whose `page_meta` predates this change. Same catch-up
10
+ * shape as {@link import('./migrate-page-meta-console-error-count.js').migratePageMetaConsoleErrorCount}.
11
+ *
12
+ * Deliberately its own file rather than an addition to
13
+ * {@link import('./migrate-main-contents-columns.js').migrateMainContentsColumns}:
14
+ * that migration early-returns once its sentinel column
15
+ * (`main_content_word_count`) already exists, so an archive that predates
16
+ * only this one column (but already has the other sixteen main-content
17
+ * columns) would never reach an appended `ALTER TABLE` inside it.
18
+ *
19
+ * No separate backfill: a page that has never been re-scraped since this
20
+ * feature shipped stays `NULL` (unknown), which is correct — it is not
21
+ * conflated with `0` (capture succeeded, zero custom elements found). See
22
+ * {@link import('./meta/compute-main-contents-denormalized.js').computeMainContentsDenormalized}.
23
+ *
24
+ * Idempotent: adding the column is a no-op once it exists.
25
+ * @param instance - The Knex query builder instance connected to the database.
26
+ * @param onLog - Called instead of `console.error` when this migration
27
+ * actually applies (issue #294: a bare `console.error` here can fire
28
+ * while a `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
29
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
30
+ * `console.error` when omitted (direct/test callers).
31
+ */
32
+ export async function migratePageMetaCustomElementCount(instance, onLog) {
33
+ const hasPageMeta = await instance.schema.hasTable('page_meta');
34
+ if (!hasPageMeta) {
35
+ return;
36
+ }
37
+ const hasColumn = await instance.schema.hasColumn('page_meta', 'main_content_custom_element_count');
38
+ if (!hasColumn) {
39
+ await instance.schema.table('page_meta', (t) => {
40
+ t.integer('main_content_custom_element_count');
41
+ });
42
+ const message = '[migrate] page_meta.main_content_custom_element_count column added';
43
+ if (onLog) {
44
+ onLog(message);
45
+ }
46
+ else {
47
+ // eslint-disable-next-line no-console
48
+ console.error(message);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,39 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Converts every `page_tags` row (the removed Wappalyzer-only detection
4
+ * table) into `technology_signals` + `page_technologies` rows, then drops
5
+ * `page_tags` — for archives crawled before this feature shipped, which
6
+ * still carry the old table.
7
+ *
8
+ * Unlike every other `migrate*` function in this directory (which add a
9
+ * nullable column and never touch existing tables), this one both writes
10
+ * to two NEW tables and DROPS an old one — because `page_technologies`
11
+ * supersedes `page_tags` outright rather than extending it. The conversion
12
+ * itself is shared with `retarget-legacy-fk-tables.ts`'s special-cased
13
+ * `page_tags` handling (0.10→0.13 upgrade path) via
14
+ * `convertLegacyPageTagsToInserts` — a `page_tags` row converts identically
15
+ * regardless of which migration path produced it.
16
+ *
17
+ * No separate backfill step is needed (v0.x policy: read-time migration
18
+ * only, no retroactive re-crawl): only the `wappalyzer` signal survives the
19
+ * conversion — the structural/`js-license-comment` signals this feature
20
+ * also detects require re-crawling to populate, which is the same
21
+ * "existing archives need a re-crawl for new signal types" trade-off
22
+ * already accepted for this feature.
23
+ *
24
+ * Idempotent: a no-op once `page_tags` is gone (either because this
25
+ * function already ran, or because the archive was crawled after this
26
+ * feature shipped and never had `page_tags` at all).
27
+ *
28
+ * Logs a row count before starting (issue #294): on a large legacy archive
29
+ * this SELECT-all-convert-drop runs as one transaction with no per-chunk
30
+ * progress, so without an upfront count it looks indistinguishable from a
31
+ * hang for however long the conversion actually takes.
32
+ * @param instance - The Knex query builder instance connected to the database.
33
+ * @param onLog - Called instead of `console.error` for both the start and
34
+ * end notices (issue #294: a bare `console.error` here can fire while a
35
+ * `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
36
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
37
+ * `console.error` when omitted (direct/test callers).
38
+ */
39
+ export declare function migratePageTagsToPageTechnologies(instance: Knex, onLog?: (message: string) => void): Promise<void>;
@@ -0,0 +1,79 @@
1
+ import { eachSplitted } from '../utils/array/each-splitted.js';
2
+ import { convertLegacyPageTagsToInserts } from './meta/technologies/convert-legacy-page-tags-to-inserts.js';
3
+ /**
4
+ * Converts every `page_tags` row (the removed Wappalyzer-only detection
5
+ * table) into `technology_signals` + `page_technologies` rows, then drops
6
+ * `page_tags` — for archives crawled before this feature shipped, which
7
+ * still carry the old table.
8
+ *
9
+ * Unlike every other `migrate*` function in this directory (which add a
10
+ * nullable column and never touch existing tables), this one both writes
11
+ * to two NEW tables and DROPS an old one — because `page_technologies`
12
+ * supersedes `page_tags` outright rather than extending it. The conversion
13
+ * itself is shared with `retarget-legacy-fk-tables.ts`'s special-cased
14
+ * `page_tags` handling (0.10→0.13 upgrade path) via
15
+ * `convertLegacyPageTagsToInserts` — a `page_tags` row converts identically
16
+ * regardless of which migration path produced it.
17
+ *
18
+ * No separate backfill step is needed (v0.x policy: read-time migration
19
+ * only, no retroactive re-crawl): only the `wappalyzer` signal survives the
20
+ * conversion — the structural/`js-license-comment` signals this feature
21
+ * also detects require re-crawling to populate, which is the same
22
+ * "existing archives need a re-crawl for new signal types" trade-off
23
+ * already accepted for this feature.
24
+ *
25
+ * Idempotent: a no-op once `page_tags` is gone (either because this
26
+ * function already ran, or because the archive was crawled after this
27
+ * feature shipped and never had `page_tags` at all).
28
+ *
29
+ * Logs a row count before starting (issue #294): on a large legacy archive
30
+ * this SELECT-all-convert-drop runs as one transaction with no per-chunk
31
+ * progress, so without an upfront count it looks indistinguishable from a
32
+ * hang for however long the conversion actually takes.
33
+ * @param instance - The Knex query builder instance connected to the database.
34
+ * @param onLog - Called instead of `console.error` for both the start and
35
+ * end notices (issue #294: a bare `console.error` here can fire while a
36
+ * `@d-zero/dealer` `Lanes`/`TaskList` display is mid-redraw during
37
+ * `Archive.open`, corrupting its cursor tracking). Falls back to
38
+ * `console.error` when omitted (direct/test callers).
39
+ */
40
+ export async function migratePageTagsToPageTechnologies(instance, onLog) {
41
+ const hasPageTags = await instance.schema.hasTable('page_tags');
42
+ if (!hasPageTags)
43
+ return;
44
+ const [row] = await instance('page_tags').count({ total: '*' });
45
+ const total = row?.total ?? 0;
46
+ const startMessage = `[migrate] converting ${total} page_tags row(s) to technology_signals/page_technologies…`;
47
+ if (onLog) {
48
+ onLog(startMessage);
49
+ }
50
+ else {
51
+ // eslint-disable-next-line no-console
52
+ console.error(startMessage);
53
+ }
54
+ await instance.transaction(async (trx) => {
55
+ const rows = await trx
56
+ .select('pageId', 'provider', 'version', 'confidence', 'categories')
57
+ .from('page_tags');
58
+ const { signalInserts, technologyInserts } = convertLegacyPageTagsToInserts(rows);
59
+ if (signalInserts.length > 0) {
60
+ await eachSplitted(signalInserts, 100, async (chunk) => {
61
+ await trx('technology_signals').insert(chunk);
62
+ });
63
+ }
64
+ if (technologyInserts.length > 0) {
65
+ await eachSplitted(technologyInserts, 100, async (chunk) => {
66
+ await trx('page_technologies').insert(chunk);
67
+ });
68
+ }
69
+ await trx.schema.dropTable('page_tags');
70
+ });
71
+ const endMessage = '[migrate] page_tags converted to technology_signals/page_technologies and dropped';
72
+ if (onLog) {
73
+ onLog(endMessage);
74
+ }
75
+ else {
76
+ // eslint-disable-next-line no-console
77
+ console.error(endMessage);
78
+ }
79
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ArchiveAccessor } from './archive-accessor.js';
2
- import type { JsonLdRow, JsonLdSummary, TagRow, TagsSummary } from './meta/types.js';
2
+ import type { JsonLdRow, JsonLdSummary, PageTechnologyRow, TechnologySignalRow } from './meta/types.js';
3
3
  import type { Anchor, Redirect, Referrer, DB_Anchor, DB_Page, DB_Redirect, DB_Referrer } from './types.js';
4
4
  /**
5
5
  * Subset of {@link DB_Page} that maps to the flat meta columns derived from
@@ -18,9 +18,10 @@ declare const FLAT_META_COLUMNS: readonly ["lang", "dir", "charset", "baseHref",
18
18
  * iterable view over all ~47 flat meta columns and {@link Page.metaExtras} for
19
19
  * the JSON catch-all of nested sub-objects not flattened to columns.
20
20
  *
21
- * JSON-LD entries and Wappalyzer tag rows live in dedicated tables and are
22
- * fetched on demand via {@link Page.getJsonLd} / {@link Page.getTags} (lazy reads, same
23
- * pattern as {@link Page.getAnchors}).
21
+ * JSON-LD entries and detected technology signals live in dedicated tables
22
+ * and are fetched on demand via {@link Page.getJsonLd} /
23
+ * {@link Page.getTechnologySignals} / {@link Page.getPageTechnologies} (lazy
24
+ * reads, same pattern as {@link Page.getAnchors}).
24
25
  *
25
26
  * Instances are created by {@link ArchiveAccessor.getPages} or
26
27
  * {@link ArchiveAccessor.getPagesWithRefs}.
@@ -155,6 +156,12 @@ export default class Page {
155
156
  * Number of canvases within the main region (denormalised aggregate), or null.
156
157
  */
157
158
  get mainContentCanvasCount(): number | null;
159
+ /**
160
+ * Number of Web Components (custom elements) within the main region
161
+ * (denormalised aggregate), `null` when unknown (page not rendered, or
162
+ * nitpicker's own capture failed — see `compute-main-contents-denormalized.ts`).
163
+ */
164
+ get mainContentCustomElementCount(): number | null;
158
165
  /**
159
166
  * Iterable view over every flat meta column (~47 fields). Returns a frozen
160
167
  * record so consumers can pick fields by name without re-enumerating
@@ -251,8 +258,9 @@ export default class Page {
251
258
  get tagCount(): number | null;
252
259
  /**
253
260
  * Sorted unique Wappalyzer provider names, comma-separated, empty string
254
- * when no tags. Denormalised aggregate; for the structured form fetch
255
- * {@link Page.getTags} (lazy).
261
+ * when no tags. Denormalised aggregate; for the structured form (with
262
+ * confidence, category, and every raw signal) fetch
263
+ * {@link Page.getTechnologySignals} / {@link Page.getPageTechnologies} (lazy).
256
264
  */
257
265
  get tagsProvidersCsv(): string;
258
266
  /**
@@ -304,6 +312,13 @@ export default class Page {
304
312
  * @returns Ordered canvas rows.
305
313
  */
306
314
  getCanvases(): Promise<import("./meta/types.js").MainContentCanvasRow[]>;
315
+ /**
316
+ * Retrieves the Web Components (custom elements) within this page's
317
+ * detected main content region from `page_main_content_custom_elements`.
318
+ * Lazy — runs a single SELECT per call.
319
+ * @returns Ordered custom-element rows.
320
+ */
321
+ getCustomElements(): Promise<import("./meta/types.js").MainContentCustomElementRow[]>;
307
322
  /**
308
323
  * Retrieves the headings within this page's detected main content region
309
324
  * from `page_main_content_headings`. Lazy — runs a single SELECT per call.
@@ -349,6 +364,12 @@ export default class Page {
349
364
  * @returns Ordered table rows.
350
365
  */
351
366
  getMainContentTables(): Promise<import("./meta/types.js").MainContentTableRow[]>;
367
+ /**
368
+ * Retrieves the confidence-combined technology roll-up for this page
369
+ * from `page_technologies`. Lazy — runs a single SELECT per call.
370
+ * @returns Technology rows, highest confidence first.
371
+ */
372
+ getPageTechnologies(): Promise<readonly PageTechnologyRow[]>;
352
373
  /**
353
374
  * Retrieves the referrers (incoming links) pointing to this page.
354
375
  * Uses pre-loaded data if available, otherwise queries the database.
@@ -362,11 +383,11 @@ export default class Page {
362
383
  */
363
384
  getRequests(): Promise<Referrer[]>;
364
385
  /**
365
- * Retrieves the Wappalyzer tag rows for this page from `page_tags`.
366
- * Lazy — runs a single SELECT per call.
367
- * @returns Ordered tag rows.
386
+ * Retrieves the raw technology-detection signals for this page from
387
+ * `technology_signals`. Lazy — runs a single SELECT per call.
388
+ * @returns Ordered signal rows.
368
389
  */
369
- getTags(): Promise<readonly TagRow[]>;
390
+ getTechnologySignals(): Promise<readonly TechnologySignalRow[]>;
370
391
  /**
371
392
  * Retrieves the videos within this page's detected main content region
372
393
  * from `page_main_content_videos`. Lazy — runs a single SELECT per call.
@@ -385,8 +406,11 @@ export default class Page {
385
406
  isPage(): boolean;
386
407
  /**
387
408
  * Serializes the page data to a plain JSON object including the full flat
388
- * meta column set, the `meta_extras` catch-all, and **summaries** of the
389
- * JSON-LD and tag rows.
409
+ * meta column set, the `meta_extras` catch-all, a **summary** of the
410
+ * JSON-LD rows, and the confidence-combined technology roll-up
411
+ * (`page_technologies` — already compact, one row per detected
412
+ * technology, so no further summarization is needed the way `page_tags`'
413
+ * per-`externalId` granularity used to require).
390
414
  *
391
415
  * Summaries (not raw entries) are inlined so a Page detail payload stays
392
416
  * token-bounded for MCP / LLM consumers — the full `raw` JSON-LD payload
@@ -400,7 +424,7 @@ export default class Page {
400
424
  toJSON(): Promise<{
401
425
  metaExtras: Record<string, unknown>;
402
426
  jsonLd: JsonLdSummary;
403
- tags: TagsSummary;
427
+ technologies: readonly PageTechnologyRow[];
404
428
  redirectFrom: Redirect[];
405
429
  isPage: boolean;
406
430
  isInternalPage: boolean;
@@ -2,7 +2,6 @@ import { tryParseUrl as parseUrl } from '@d-zero/shared/parse-url';
2
2
  import { isHtmlContentType } from '../crawler/is-html-content-type.js';
3
3
  import { parseResponseHeaders } from '../utils/object/parse-response-headers.js';
4
4
  import { summarizeJsonLd } from './meta/summarize-jsonld.js';
5
- import { summarizeTags } from './meta/summarize-tags.js';
6
5
  /**
7
6
  * Subset of {@link DB_Page} that maps to the flat meta columns derived from
8
7
  * beholder's nested Meta. Used by {@link Page.metaFlat} so consumers can
@@ -86,9 +85,10 @@ const FLAT_META_COLUMNS = [
86
85
  * iterable view over all ~47 flat meta columns and {@link Page.metaExtras} for
87
86
  * the JSON catch-all of nested sub-objects not flattened to columns.
88
87
  *
89
- * JSON-LD entries and Wappalyzer tag rows live in dedicated tables and are
90
- * fetched on demand via {@link Page.getJsonLd} / {@link Page.getTags} (lazy reads, same
91
- * pattern as {@link Page.getAnchors}).
88
+ * JSON-LD entries and detected technology signals live in dedicated tables
89
+ * and are fetched on demand via {@link Page.getJsonLd} /
90
+ * {@link Page.getTechnologySignals} / {@link Page.getPageTechnologies} (lazy
91
+ * reads, same pattern as {@link Page.getAnchors}).
92
92
  *
93
93
  * Instances are created by {@link ArchiveAccessor.getPages} or
94
94
  * {@link ArchiveAccessor.getPagesWithRefs}.
@@ -286,6 +286,14 @@ export default class Page {
286
286
  get mainContentCanvasCount() {
287
287
  return this.#raw.main_content_canvas_count;
288
288
  }
289
+ /**
290
+ * Number of Web Components (custom elements) within the main region
291
+ * (denormalised aggregate), `null` when unknown (page not rendered, or
292
+ * nitpicker's own capture failed — see `compute-main-contents-denormalized.ts`).
293
+ */
294
+ get mainContentCustomElementCount() {
295
+ return this.#raw.main_content_custom_element_count;
296
+ }
289
297
  /**
290
298
  * Iterable view over every flat meta column (~47 fields). Returns a frozen
291
299
  * record so consumers can pick fields by name without re-enumerating
@@ -436,8 +444,9 @@ export default class Page {
436
444
  }
437
445
  /**
438
446
  * Sorted unique Wappalyzer provider names, comma-separated, empty string
439
- * when no tags. Denormalised aggregate; for the structured form fetch
440
- * {@link Page.getTags} (lazy).
447
+ * when no tags. Denormalised aggregate; for the structured form (with
448
+ * confidence, category, and every raw signal) fetch
449
+ * {@link Page.getTechnologySignals} / {@link Page.getPageTechnologies} (lazy).
441
450
  */
442
451
  get tagsProvidersCsv() {
443
452
  return this.#raw.tags_providers_csv ?? '';
@@ -530,6 +539,15 @@ export default class Page {
530
539
  async getCanvases() {
531
540
  return this.#archive.getCanvasesOfPage(this.#raw.id);
532
541
  }
542
+ /**
543
+ * Retrieves the Web Components (custom elements) within this page's
544
+ * detected main content region from `page_main_content_custom_elements`.
545
+ * Lazy — runs a single SELECT per call.
546
+ * @returns Ordered custom-element rows.
547
+ */
548
+ async getCustomElements() {
549
+ return this.#archive.getCustomElementsOfPage(this.#raw.id);
550
+ }
533
551
  /**
534
552
  * Retrieves the headings within this page's detected main content region
535
553
  * from `page_main_content_headings`. Lazy — runs a single SELECT per call.
@@ -587,6 +605,14 @@ export default class Page {
587
605
  async getMainContentTables() {
588
606
  return this.#archive.getMainContentTablesOfPage(this.#raw.id);
589
607
  }
608
+ /**
609
+ * Retrieves the confidence-combined technology roll-up for this page
610
+ * from `page_technologies`. Lazy — runs a single SELECT per call.
611
+ * @returns Technology rows, highest confidence first.
612
+ */
613
+ async getPageTechnologies() {
614
+ return this.#archive.getPageTechnologiesOfPage(this.#raw.id);
615
+ }
590
616
  /**
591
617
  * Retrieves the referrers (incoming links) pointing to this page.
592
618
  * Uses pre-loaded data if available, otherwise queries the database.
@@ -627,12 +653,12 @@ export default class Page {
627
653
  }));
628
654
  }
629
655
  /**
630
- * Retrieves the Wappalyzer tag rows for this page from `page_tags`.
631
- * Lazy — runs a single SELECT per call.
632
- * @returns Ordered tag rows.
656
+ * Retrieves the raw technology-detection signals for this page from
657
+ * `technology_signals`. Lazy — runs a single SELECT per call.
658
+ * @returns Ordered signal rows.
633
659
  */
634
- async getTags() {
635
- return this.#archive.getTagsOfPage(this.#raw.id);
660
+ async getTechnologySignals() {
661
+ return this.#archive.getTechnologySignalsOfPage(this.#raw.id);
636
662
  }
637
663
  /**
638
664
  * Retrieves the videos within this page's detected main content region
@@ -658,8 +684,11 @@ export default class Page {
658
684
  }
659
685
  /**
660
686
  * Serializes the page data to a plain JSON object including the full flat
661
- * meta column set, the `meta_extras` catch-all, and **summaries** of the
662
- * JSON-LD and tag rows.
687
+ * meta column set, the `meta_extras` catch-all, a **summary** of the
688
+ * JSON-LD rows, and the confidence-combined technology roll-up
689
+ * (`page_technologies` — already compact, one row per detected
690
+ * technology, so no further summarization is needed the way `page_tags`'
691
+ * per-`externalId` granularity used to require).
663
692
  *
664
693
  * Summaries (not raw entries) are inlined so a Page detail payload stays
665
694
  * token-bounded for MCP / LLM consumers — the full `raw` JSON-LD payload
@@ -671,14 +700,13 @@ export default class Page {
671
700
  * @returns A plain object containing all page metadata and relationships.
672
701
  */
673
702
  async toJSON() {
674
- const [anchors, referrers, jsonLdRows, tagRows] = await Promise.all([
703
+ const [anchors, referrers, jsonLdRows, technologies] = await Promise.all([
675
704
  this.getAnchors(),
676
705
  this.getReferrers(),
677
706
  this.getJsonLd(),
678
- this.getTags(),
707
+ this.getPageTechnologies(),
679
708
  ]);
680
709
  const jsonLdSummary = summarizeJsonLd(jsonLdRows);
681
- const tagsSummary = summarizeTags(tagRows);
682
710
  return {
683
711
  url: this.url.href,
684
712
  status: this.status,
@@ -695,7 +723,7 @@ export default class Page {
695
723
  ...this.metaFlat,
696
724
  metaExtras: this.metaExtras,
697
725
  jsonLd: jsonLdSummary,
698
- tags: tagsSummary,
726
+ technologies,
699
727
  redirectFrom: this.redirectFrom,
700
728
  isPage: this.isPage(),
701
729
  isInternalPage: this.isInternalPage(),