@nitpicker/crawler 0.18.0 → 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 (140) 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 +24 -4
  35. package/lib/archive/db-ops/pages/write/insert-page.js +40 -4
  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/link-redirect-sources.d.ts +7 -1
  39. package/lib/archive/db-ops/pages/write/link-redirect-sources.js +8 -2
  40. package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
  41. package/lib/archive/db-ops/pages/write/update-page.js +38 -16
  42. package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
  43. package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
  44. package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
  45. package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
  46. package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
  47. package/lib/archive/filesystem/parse-pax-path.js +47 -0
  48. package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
  49. package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
  50. package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
  51. package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
  52. package/lib/archive/filesystem/tar.d.ts +13 -1
  53. package/lib/archive/filesystem/tar.js +63 -7
  54. package/lib/archive/filesystem/untar.d.ts +13 -0
  55. package/lib/archive/filesystem/untar.js +68 -8
  56. package/lib/archive/init-schema.d.ts +1 -1
  57. package/lib/archive/init-schema.js +6 -4
  58. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
  59. package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
  60. package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
  61. package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
  62. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
  63. package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
  64. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
  65. package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
  66. package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
  67. package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
  68. package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
  69. package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
  70. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
  71. package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
  72. package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
  73. package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
  74. package/lib/archive/meta/technologies/types.d.ts +68 -0
  75. package/lib/archive/meta/technologies/types.js +13 -0
  76. package/lib/archive/meta/types.d.ts +97 -30
  77. package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
  78. package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
  79. package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
  80. package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
  81. package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
  82. package/lib/archive/migrate-info-main-content-selector.js +14 -3
  83. package/lib/archive/migrate-info-roots.d.ts +6 -1
  84. package/lib/archive/migrate-info-roots.js +14 -3
  85. package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
  86. package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
  87. package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
  88. package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
  89. package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
  90. package/lib/archive/migrate-main-contents-columns.js +14 -3
  91. package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
  92. package/lib/archive/migrate-page-meta-body-hash.js +14 -3
  93. package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
  94. package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
  95. package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
  96. package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
  97. package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
  98. package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
  99. package/lib/archive/page.d.ts +37 -13
  100. package/lib/archive/page.js +45 -17
  101. package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
  102. package/lib/archive/retarget-legacy-fk-tables.js +61 -15
  103. package/lib/archive/types.d.ts +18 -0
  104. package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
  105. package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
  106. package/lib/crawler/capture-custom-elements.d.ts +33 -0
  107. package/lib/crawler/capture-custom-elements.js +39 -0
  108. package/lib/crawler/close-browser-safely.d.ts +3 -3
  109. package/lib/crawler/close-browser-safely.js +8 -17
  110. package/lib/crawler/collect-custom-elements.d.ts +33 -0
  111. package/lib/crawler/collect-custom-elements.js +110 -0
  112. package/lib/crawler/crawler.js +101 -46
  113. package/lib/crawler/fetch-destination.js +6 -13
  114. package/lib/crawler/find-package-dir.d.ts +24 -0
  115. package/lib/crawler/find-package-dir.js +39 -0
  116. package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
  117. package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
  118. package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
  119. package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
  120. package/lib/crawler/types.d.ts +23 -0
  121. package/lib/crawler-orchestrator.d.ts +53 -9
  122. package/lib/crawler-orchestrator.js +223 -46
  123. package/lib/crawler.d.ts +11 -1
  124. package/lib/crawler.js +9 -0
  125. package/lib/inventory-setup-phases.d.ts +32 -0
  126. package/lib/inventory-setup-phases.js +45 -0
  127. package/lib/resume-setup-phases.d.ts +16 -0
  128. package/lib/resume-setup-phases.js +24 -0
  129. package/lib/retry-failed-setup-phases.d.ts +19 -0
  130. package/lib/retry-failed-setup-phases.js +24 -0
  131. package/lib/setup-recovery-phase-labels.d.ts +21 -0
  132. package/lib/setup-recovery-phase-labels.js +24 -0
  133. package/lib/types.d.ts +145 -0
  134. package/package.json +12 -9
  135. package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
  136. package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
  137. package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
  138. package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
  139. package/lib/archive/meta/summarize-tags.d.ts +0 -16
  140. package/lib/archive/meta/summarize-tags.js +0 -33
@@ -13,11 +13,22 @@
13
13
  * omit it (`.spec.ts` files are excluded from the `tsc` build, so this goes
14
14
  * uncaught at compile time) — `== null` tolerates both without forcing every
15
15
  * fixture to be updated.
16
+ *
17
+ * `customElementCount` is a separate parameter, not read off `mainContents`,
18
+ * because it is not one of beholder's `MainContentsData` categories —
19
+ * nitpicker captures Web Components itself (`crawler/capture-custom-elements.ts`)
20
+ * after `scrapeStart` returns. It carries three distinct states:
21
+ * `undefined`/capture-not-attempted and capture-failure both collapse to
22
+ * `null` (unknown — NOT the same as "captured, zero found"), while any
23
+ * number (including `0`) means capture succeeded.
16
24
  * @param mainContents - Beholder's per-page main-content metrics, or `null`/`undefined`.
17
25
  * @param scrollHeight - Beholder's per-page scroll-height measurements, or `null`/`undefined`.
18
- * @returns The seventeen denormalised columns.
26
+ * @param customElementCount - Count of Web Components nitpicker captured in
27
+ * the main-content region, or `null`/`undefined` when capture was not
28
+ * attempted or failed.
29
+ * @returns The eighteen denormalised columns.
19
30
  */
20
- export function computeMainContentsDenormalized(mainContents, scrollHeight) {
31
+ export function computeMainContentsDenormalized(mainContents, scrollHeight, customElementCount) {
21
32
  if (mainContents == null) {
22
33
  return {
23
34
  main_content_node_name: null,
@@ -35,6 +46,7 @@ export function computeMainContentsDenormalized(mainContents, scrollHeight) {
35
46
  main_content_video_count: null,
36
47
  main_content_audio_count: null,
37
48
  main_content_canvas_count: null,
49
+ main_content_custom_element_count: null,
38
50
  scroll_height_desktop: null,
39
51
  scroll_height_mobile: null,
40
52
  };
@@ -57,6 +69,7 @@ export function computeMainContentsDenormalized(mainContents, scrollHeight) {
57
69
  main_content_video_count: mainContents.videos.length,
58
70
  main_content_audio_count: mainContents.audios.length,
59
71
  main_content_canvas_count: mainContents.canvases.length,
72
+ main_content_custom_element_count: customElementCount ?? null,
60
73
  scroll_height_desktop: scrollHeight?.desktop ?? null,
61
74
  scroll_height_mobile: scrollHeight?.mobile ?? null,
62
75
  };
@@ -0,0 +1,46 @@
1
+ import type { PageTechnologyPartial, TechnologySignalPartial } from './types.js';
2
+ /**
3
+ * Options for {@link combineTechnologyConfidence}.
4
+ */
5
+ export interface CombineTechnologyConfidenceOptions {
6
+ /**
7
+ * Skip the `TECHNOLOGY_CONFIDENCE_THRESHOLD` drop. Set by the `page_tags`
8
+ * legacy-migration callers (`migrate-page-tags-to-page-technologies.ts`,
9
+ * `retarget-legacy-fk-tables.ts`): the threshold exists to filter noisy
10
+ * NEW detections at crawl time, but migration is converting Wappalyzer
11
+ * detections the old `page_tags`-based API already returned
12
+ * unconditionally (including low-confidence ones) — dropping them here
13
+ * would silently and permanently lose data with no recovery path (v0.x
14
+ * policy is read-time migration only, no retroactive re-crawl).
15
+ */
16
+ skipThreshold?: boolean;
17
+ }
18
+ /**
19
+ * Combines every signal detected for one technology on one page into a
20
+ * single confidence score via noisy-OR:
21
+ *
22
+ * ```
23
+ * confidence = round(100 * (1 - Π (1 - weight/100)))
24
+ * ```
25
+ *
26
+ * over each DISTINCT `signalType` present (same signal type appearing
27
+ * twice — e.g. two different `url-pattern` definitions both matching —
28
+ * counts once, at its highest weight; compounding only happens ACROSS
29
+ * signal types, which is what makes this "multiple independent signals
30
+ * agree" rather than "one noisy signal repeated"). A single signal's
31
+ * result equals its own weight exactly (`1 - (1 - w/100) = w/100`).
32
+ *
33
+ * Technologies whose combined confidence falls below
34
+ * `TECHNOLOGY_CONFIDENCE_THRESHOLD` are dropped entirely — a lone weak
35
+ * signal (e.g. `id="app"`, weight 15) is noise, not a detection, and
36
+ * `page_technologies` has no `isDetected` column to hide it behind; the
37
+ * row simply is not written. Pass `options.skipThreshold` to keep every
38
+ * technology regardless of confidence (see its docs for why).
39
+ * @param signals - Every signal detected for a single page, across all
40
+ * technologies (this function groups by `technology` internally).
41
+ * @param options - See {@link CombineTechnologyConfidenceOptions}.
42
+ * @returns One {@link PageTechnologyPartial} per technology (filtered by
43
+ * the confidence threshold unless `options.skipThreshold` is set), in no
44
+ * particular order.
45
+ */
46
+ export declare function combineTechnologyConfidence(signals: readonly TechnologySignalPartial[], options?: CombineTechnologyConfidenceOptions): PageTechnologyPartial[];
@@ -0,0 +1,93 @@
1
+ import { TECHNOLOGY_CONFIDENCE_THRESHOLD } from './technology-signal-definitions.js';
2
+ /**
3
+ * Priority order for picking which signal's `category`/`version` wins when
4
+ * more than one signal for the same technology supplies one. `wappalyzer`
5
+ * wins first — it is the only source with a real Wappalyzer-reported
6
+ * `version`; `meta-generator` second — also carries a real version
7
+ * (`<meta name="generator" content="Astro v4.2.0">`); the rest never
8
+ * carry a version and are listed only so the `.find()` below terminates
9
+ * deterministically regardless of definition order.
10
+ */
11
+ const CATEGORY_VERSION_SOURCE_PRIORITY = [
12
+ 'wappalyzer',
13
+ 'meta-generator',
14
+ 'html-marker',
15
+ 'url-pattern',
16
+ 'scoped-attr',
17
+ 'js-license-comment',
18
+ 'weak-marker',
19
+ ];
20
+ /**
21
+ * Combines every signal detected for one technology on one page into a
22
+ * single confidence score via noisy-OR:
23
+ *
24
+ * ```
25
+ * confidence = round(100 * (1 - Π (1 - weight/100)))
26
+ * ```
27
+ *
28
+ * over each DISTINCT `signalType` present (same signal type appearing
29
+ * twice — e.g. two different `url-pattern` definitions both matching —
30
+ * counts once, at its highest weight; compounding only happens ACROSS
31
+ * signal types, which is what makes this "multiple independent signals
32
+ * agree" rather than "one noisy signal repeated"). A single signal's
33
+ * result equals its own weight exactly (`1 - (1 - w/100) = w/100`).
34
+ *
35
+ * Technologies whose combined confidence falls below
36
+ * `TECHNOLOGY_CONFIDENCE_THRESHOLD` are dropped entirely — a lone weak
37
+ * signal (e.g. `id="app"`, weight 15) is noise, not a detection, and
38
+ * `page_technologies` has no `isDetected` column to hide it behind; the
39
+ * row simply is not written. Pass `options.skipThreshold` to keep every
40
+ * technology regardless of confidence (see its docs for why).
41
+ * @param signals - Every signal detected for a single page, across all
42
+ * technologies (this function groups by `technology` internally).
43
+ * @param options - See {@link CombineTechnologyConfidenceOptions}.
44
+ * @returns One {@link PageTechnologyPartial} per technology (filtered by
45
+ * the confidence threshold unless `options.skipThreshold` is set), in no
46
+ * particular order.
47
+ */
48
+ export function combineTechnologyConfidence(signals, options = {}) {
49
+ const byTechnology = new Map();
50
+ for (const signal of signals) {
51
+ const group = byTechnology.get(signal.technology);
52
+ if (group) {
53
+ group.push(signal);
54
+ }
55
+ else {
56
+ byTechnology.set(signal.technology, [signal]);
57
+ }
58
+ }
59
+ const results = [];
60
+ for (const [technology, group] of byTechnology) {
61
+ const maxWeightByType = new Map();
62
+ for (const signal of group) {
63
+ const current = maxWeightByType.get(signal.signalType);
64
+ if (current === undefined || signal.weight > current) {
65
+ maxWeightByType.set(signal.signalType, signal.weight);
66
+ }
67
+ }
68
+ const product = [...maxWeightByType.values()].reduce((acc, weight) => acc * (1 - weight / 100), 1);
69
+ const confidence = Math.round(100 * (1 - product));
70
+ if (!options.skipThreshold && confidence < TECHNOLOGY_CONFIDENCE_THRESHOLD)
71
+ continue;
72
+ let categorySource;
73
+ let versionSource;
74
+ for (const type of CATEGORY_VERSION_SOURCE_PRIORITY) {
75
+ if (!categorySource) {
76
+ categorySource = group.find((s) => s.signalType === type && s.category != null);
77
+ }
78
+ if (!versionSource) {
79
+ versionSource = group.find((s) => s.signalType === type && s.version != null);
80
+ }
81
+ if (categorySource && versionSource)
82
+ break;
83
+ }
84
+ results.push({
85
+ technology,
86
+ category: categorySource?.category ?? null,
87
+ version: versionSource?.version ?? null,
88
+ confidence,
89
+ signalCount: maxWeightByType.size,
90
+ });
91
+ }
92
+ return results;
93
+ }
@@ -0,0 +1,45 @@
1
+ import type { LegacyTagRowLike } from './convert-tag-row-to-wappalyzer-signal.js';
2
+ /** One `technology_signals` row ready for insertion, `pageId` already resolved. */
3
+ export interface LegacyTechnologySignalInsert {
4
+ pageId: number;
5
+ technology: string;
6
+ signalType: string;
7
+ evidence: string | null;
8
+ weight: number;
9
+ }
10
+ /** One `page_technologies` row ready for insertion, `pageId` already resolved. */
11
+ export interface LegacyPageTechnologyInsert {
12
+ pageId: number;
13
+ technology: string;
14
+ category: string | null;
15
+ version: string | null;
16
+ confidence: number;
17
+ signalCount: number;
18
+ }
19
+ /** Result of {@link convertLegacyPageTagsToInserts}. */
20
+ export interface LegacyPageTagsConversion {
21
+ signalInserts: LegacyTechnologySignalInsert[];
22
+ technologyInserts: LegacyPageTechnologyInsert[];
23
+ }
24
+ /**
25
+ * Converts every persisted (legacy) `page_tags` row into the `pageId`-keyed
26
+ * `technology_signals` + `page_technologies` insert rows for that page's
27
+ * whole tag set — shared by both `page_tags` migration paths
28
+ * (`migrate-page-tags-to-page-technologies.ts` for archives already on the
29
+ * current schema, `retarget-legacy-fk-tables.ts` for the 0.10→0.13 upgrade)
30
+ * so a `page_tags` row converts identically regardless of which path
31
+ * produced it, and a future fix to the conversion only needs to change one
32
+ * place.
33
+ *
34
+ * Uses `combineTechnologyConfidence(signals, { skipThreshold: true })` —
35
+ * migration preserves every historical Wappalyzer detection the old
36
+ * `page_tags`-based API returned unconditionally, rather than applying the
37
+ * confidence-noise filter meant for fresh crawl-time detections (see that
38
+ * option's docs).
39
+ * @param rows - Every `page_tags` row, in any order (grouped internally by
40
+ * `pageId`).
41
+ * @returns The two insert-ready row sets, empty arrays if `rows` is empty.
42
+ */
43
+ export declare function convertLegacyPageTagsToInserts(rows: readonly (LegacyTagRowLike & {
44
+ pageId: number;
45
+ })[]): LegacyPageTagsConversion;
@@ -0,0 +1,53 @@
1
+ import { combineTechnologyConfidence } from './combine-technology-confidence.js';
2
+ import { convertTagRowToWappalyzerSignal } from './convert-tag-row-to-wappalyzer-signal.js';
3
+ /**
4
+ * Converts every persisted (legacy) `page_tags` row into the `pageId`-keyed
5
+ * `technology_signals` + `page_technologies` insert rows for that page's
6
+ * whole tag set — shared by both `page_tags` migration paths
7
+ * (`migrate-page-tags-to-page-technologies.ts` for archives already on the
8
+ * current schema, `retarget-legacy-fk-tables.ts` for the 0.10→0.13 upgrade)
9
+ * so a `page_tags` row converts identically regardless of which path
10
+ * produced it, and a future fix to the conversion only needs to change one
11
+ * place.
12
+ *
13
+ * Uses `combineTechnologyConfidence(signals, { skipThreshold: true })` —
14
+ * migration preserves every historical Wappalyzer detection the old
15
+ * `page_tags`-based API returned unconditionally, rather than applying the
16
+ * confidence-noise filter meant for fresh crawl-time detections (see that
17
+ * option's docs).
18
+ * @param rows - Every `page_tags` row, in any order (grouped internally by
19
+ * `pageId`).
20
+ * @returns The two insert-ready row sets, empty arrays if `rows` is empty.
21
+ */
22
+ export function convertLegacyPageTagsToInserts(rows) {
23
+ const signalsByPage = new Map();
24
+ for (const row of rows) {
25
+ const signal = convertTagRowToWappalyzerSignal(row);
26
+ const group = signalsByPage.get(row.pageId);
27
+ if (group) {
28
+ group.push(signal);
29
+ }
30
+ else {
31
+ signalsByPage.set(row.pageId, [signal]);
32
+ }
33
+ }
34
+ const signalInserts = [];
35
+ const technologyInserts = [];
36
+ for (const [pageId, signals] of signalsByPage) {
37
+ for (const signal of signals) {
38
+ signalInserts.push({
39
+ pageId,
40
+ technology: signal.technology,
41
+ signalType: signal.signalType,
42
+ evidence: signal.evidence,
43
+ weight: signal.weight,
44
+ });
45
+ }
46
+ for (const technology of combineTechnologyConfidence(signals, {
47
+ skipThreshold: true,
48
+ })) {
49
+ technologyInserts.push({ pageId, ...technology });
50
+ }
51
+ }
52
+ return { signalInserts, technologyInserts };
53
+ }
@@ -0,0 +1,33 @@
1
+ import type { TechnologySignalPartial } from './types.js';
2
+ /**
3
+ * Minimal shape this function needs from a persisted (legacy) `page_tags`
4
+ * row. Deliberately narrower than the old `TagRow` — only the columns a
5
+ * `wappalyzer` signal can use. `categories` accepts `unknown` because
6
+ * callers read it via two different paths that disagree on whether the
7
+ * driver already parsed the `json()` column: raw `SELECT` (still a JSON
8
+ * string) vs. knex's typed row mapping (already an array on some drivers).
9
+ */
10
+ export interface LegacyTagRowLike {
11
+ provider: string;
12
+ version: string | null;
13
+ confidence: number | null;
14
+ categories: unknown;
15
+ }
16
+ /**
17
+ * Converts one persisted (legacy) `page_tags` row into a `wappalyzer`
18
+ * technology signal — the shared core `migrate-page-tags-to-page-technologies.ts`
19
+ * (read-time migration for archives already on the current schema) and
20
+ * `retarget-legacy-fk-tables.ts` (0.10→0.13 upgrade path) both call, so a
21
+ * `page_tags` row converts identically regardless of which migration path
22
+ * produced it.
23
+ *
24
+ * Mirrors `normalize-wappalyzer-entries.ts`'s live-beholder conversion, but
25
+ * reads from the OLD persisted row shape (`provider`/`version`/`confidence`/
26
+ * `categories`) instead of beholder's live `TagEntry` (`provider`/`version`/
27
+ * `confidence`/`categories`/`id`/`sources`) — structurally similar, but
28
+ * `page_tags` has no `id` (externalId) to use as `evidence`, so the provider
29
+ * name is used instead.
30
+ * @param row - The persisted `page_tags` row (or an equivalent shape).
31
+ * @returns The equivalent `wappalyzer` signal.
32
+ */
33
+ export declare function convertTagRowToWappalyzerSignal(row: LegacyTagRowLike): TechnologySignalPartial;
@@ -0,0 +1,46 @@
1
+ import { WAPPALYZER_PROVIDER_TO_TECHNOLOGY, WAPPALYZER_SIGNAL_WEIGHT, } from './technology-signal-definitions.js';
2
+ /**
3
+ * Converts one persisted (legacy) `page_tags` row into a `wappalyzer`
4
+ * technology signal — the shared core `migrate-page-tags-to-page-technologies.ts`
5
+ * (read-time migration for archives already on the current schema) and
6
+ * `retarget-legacy-fk-tables.ts` (0.10→0.13 upgrade path) both call, so a
7
+ * `page_tags` row converts identically regardless of which migration path
8
+ * produced it.
9
+ *
10
+ * Mirrors `normalize-wappalyzer-entries.ts`'s live-beholder conversion, but
11
+ * reads from the OLD persisted row shape (`provider`/`version`/`confidence`/
12
+ * `categories`) instead of beholder's live `TagEntry` (`provider`/`version`/
13
+ * `confidence`/`categories`/`id`/`sources`) — structurally similar, but
14
+ * `page_tags` has no `id` (externalId) to use as `evidence`, so the provider
15
+ * name is used instead.
16
+ * @param row - The persisted `page_tags` row (or an equivalent shape).
17
+ * @returns The equivalent `wappalyzer` signal.
18
+ */
19
+ export function convertTagRowToWappalyzerSignal(row) {
20
+ const categories = parseCategories(row.categories);
21
+ return {
22
+ technology: WAPPALYZER_PROVIDER_TO_TECHNOLOGY[row.provider] ?? row.provider,
23
+ signalType: 'wappalyzer',
24
+ evidence: row.provider,
25
+ weight: row.confidence ?? WAPPALYZER_SIGNAL_WEIGHT,
26
+ category: categories[0] ?? null,
27
+ version: row.version ?? null,
28
+ };
29
+ }
30
+ /**
31
+ * @param raw
32
+ */
33
+ function parseCategories(raw) {
34
+ if (Array.isArray(raw))
35
+ return raw;
36
+ if (typeof raw === 'string' && raw.length > 0) {
37
+ try {
38
+ const parsed = JSON.parse(raw);
39
+ return Array.isArray(parsed) ? parsed : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ return [];
46
+ }
@@ -0,0 +1,30 @@
1
+ import type { PageTechnologyPartial, TechnologySignalPartial } from './types.js';
2
+ import type { PageData } from '../../../utils/types/types.js';
3
+ /**
4
+ * The result of one page's technology-signal extraction: every raw signal
5
+ * (for `technology_signals`, MCP/query signal-level lookups, and per-page
6
+ * "why was this detected" drill-down) plus the confidence-combined roll-up
7
+ * (for `page_technologies`).
8
+ */
9
+ export interface ExtractedTechnologies {
10
+ signals: TechnologySignalPartial[];
11
+ technologies: PageTechnologyPartial[];
12
+ }
13
+ /**
14
+ * Extracts every technology signal for one page — structural (HTML markers,
15
+ * URL path fragments, scoped attributes, `<meta name="generator">`) plus
16
+ * Wappalyzer (beholder's `meta.tags.entries`) — and combines them into
17
+ * confidence-scored technology rows.
18
+ *
19
+ * Crawl-time, synchronous, zero extra network cost — the direct replacement
20
+ * for `extract-tags-for-archive.ts` in the live crawl write path (that file
21
+ * itself is untouched; `scripts/migrate-to-0.10.mjs` still depends on it).
22
+ * JS-license-comment signals are NOT produced here — they require an extra
23
+ * network fetch per JS resource and are added afterward, once per crawl, by
24
+ * `scan-js-resources-for-technology-signals.ts` ("JSスキャン・エンリッチメント",
25
+ * a distinct post-crawl category — see ARCHITECTURE.md).
26
+ * @param html - The page's raw HTML string.
27
+ * @param meta - `PageData.meta` (for `generator` and `tags.entries`).
28
+ * @returns Raw signals and their confidence-combined roll-up.
29
+ */
30
+ export declare function extractTechnologiesForArchive(html: string, meta: PageData['meta']): ExtractedTechnologies;
@@ -0,0 +1,27 @@
1
+ import { combineTechnologyConfidence } from './combine-technology-confidence.js';
2
+ import { matchStructuralTechnologySignals } from './match-structural-technology-signals.js';
3
+ import { normalizeWappalyzerEntries } from './normalize-wappalyzer-entries.js';
4
+ /**
5
+ * Extracts every technology signal for one page — structural (HTML markers,
6
+ * URL path fragments, scoped attributes, `<meta name="generator">`) plus
7
+ * Wappalyzer (beholder's `meta.tags.entries`) — and combines them into
8
+ * confidence-scored technology rows.
9
+ *
10
+ * Crawl-time, synchronous, zero extra network cost — the direct replacement
11
+ * for `extract-tags-for-archive.ts` in the live crawl write path (that file
12
+ * itself is untouched; `scripts/migrate-to-0.10.mjs` still depends on it).
13
+ * JS-license-comment signals are NOT produced here — they require an extra
14
+ * network fetch per JS resource and are added afterward, once per crawl, by
15
+ * `scan-js-resources-for-technology-signals.ts` ("JSスキャン・エンリッチメント",
16
+ * a distinct post-crawl category — see ARCHITECTURE.md).
17
+ * @param html - The page's raw HTML string.
18
+ * @param meta - `PageData.meta` (for `generator` and `tags.entries`).
19
+ * @returns Raw signals and their confidence-combined roll-up.
20
+ */
21
+ export function extractTechnologiesForArchive(html, meta) {
22
+ const structuralSignals = matchStructuralTechnologySignals(html, meta.generator);
23
+ const wappalyzerSignals = normalizeWappalyzerEntries(meta.tags?.entries);
24
+ const signals = [...structuralSignals, ...wappalyzerSignals];
25
+ const technologies = combineTechnologyConfidence(signals);
26
+ return { signals, technologies };
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { TechnologySignalPartial } from './types.js';
2
+ /**
3
+ * Detects every {@link TECHNOLOGY_SIGNAL_DEFINITIONS} pattern against a
4
+ * page's raw HTML string, plus `<meta name="generator">` prefix matches
5
+ * against the already-extracted `generator` field. Pure and synchronous —
6
+ * no network I/O, no Wappalyzer data (see `normalize-wappalyzer-entries.ts`
7
+ * for that source).
8
+ * @param html - The page's raw HTML string.
9
+ * @param generator - `page_meta.generator` (already extracted by
10
+ * `derive-flat-from-meta.ts` from beholder's `Meta`), or `null`/`undefined`.
11
+ * @returns Un-combined structural signals, one per matched definition.
12
+ */
13
+ export declare function matchStructuralTechnologySignals(html: string, generator: string | null | undefined): TechnologySignalPartial[];
@@ -0,0 +1,44 @@
1
+ import { GENERATOR_TECHNOLOGY_MATCHERS, TECHNOLOGY_SIGNAL_DEFINITIONS, } from './technology-signal-definitions.js';
2
+ const EVIDENCE_MAX_LENGTH = 200;
3
+ /**
4
+ * Detects every {@link TECHNOLOGY_SIGNAL_DEFINITIONS} pattern against a
5
+ * page's raw HTML string, plus `<meta name="generator">` prefix matches
6
+ * against the already-extracted `generator` field. Pure and synchronous —
7
+ * no network I/O, no Wappalyzer data (see `normalize-wappalyzer-entries.ts`
8
+ * for that source).
9
+ * @param html - The page's raw HTML string.
10
+ * @param generator - `page_meta.generator` (already extracted by
11
+ * `derive-flat-from-meta.ts` from beholder's `Meta`), or `null`/`undefined`.
12
+ * @returns Un-combined structural signals, one per matched definition.
13
+ */
14
+ export function matchStructuralTechnologySignals(html, generator) {
15
+ const signals = [];
16
+ for (const def of TECHNOLOGY_SIGNAL_DEFINITIONS) {
17
+ const match = def.pattern.exec(html);
18
+ if (match) {
19
+ signals.push({
20
+ technology: def.technology,
21
+ signalType: def.signalType,
22
+ evidence: match[0].slice(0, EVIDENCE_MAX_LENGTH),
23
+ weight: def.weight,
24
+ category: def.category ?? null,
25
+ });
26
+ }
27
+ }
28
+ if (generator) {
29
+ const lowerGenerator = generator.toLowerCase();
30
+ for (const matcher of GENERATOR_TECHNOLOGY_MATCHERS) {
31
+ if (lowerGenerator.startsWith(matcher.prefix)) {
32
+ signals.push({
33
+ technology: matcher.technology,
34
+ signalType: 'meta-generator',
35
+ evidence: generator.slice(0, EVIDENCE_MAX_LENGTH),
36
+ weight: matcher.weight,
37
+ category: matcher.category,
38
+ version: generator.slice(matcher.prefix.length).trim() || null,
39
+ });
40
+ }
41
+ }
42
+ }
43
+ return signals;
44
+ }
@@ -0,0 +1,25 @@
1
+ import type { TechnologySignalPartial } from './types.js';
2
+ import type { TagEntry } from '@d-zero/beholder';
3
+ /**
4
+ * Converts beholder's `TagsMeta.entries` (per-page Wappalyzer detections,
5
+ * one row per provider × external-id tuple) into `wappalyzer` technology
6
+ * signals.
7
+ *
8
+ * Every entry becomes a signal — Wappalyzer is not limited to the curated
9
+ * frameworks {@link TECHNOLOGY_SIGNAL_DEFINITIONS} tracks; analytics tools,
10
+ * CMSes, and everything else Wappalyzer's fingerprint DB knows about pass
11
+ * through as their own technology (provider name verbatim). Only the
12
+ * curated frameworks get their provider name FOLDED onto the same
13
+ * technology name their structural signals use, via
14
+ * {@link WAPPALYZER_PROVIDER_TO_TECHNOLOGY} — so e.g. Wappalyzer's `'Vue.js'`
15
+ * and a `data-v-*` structural match both contribute to the `'Vue'` row,
16
+ * not two separate rows.
17
+ *
18
+ * This is the direct replacement for the old `extract-tags-for-archive.ts`
19
+ * (which still exists, unmodified, for `scripts/migrate-to-0.10.mjs`'s
20
+ * independent use — see that file's JSDoc) in the LIVE crawl write path.
21
+ * @param entries - `meta.tags.entries` from beholder, or `undefined`
22
+ * (legacy minimal-`Meta` test fixtures predate the required field).
23
+ * @returns One signal per entry; empty array when no tags were detected.
24
+ */
25
+ export declare function normalizeWappalyzerEntries(entries: readonly TagEntry[] | undefined): TechnologySignalPartial[];
@@ -0,0 +1,35 @@
1
+ import { WAPPALYZER_PROVIDER_TO_TECHNOLOGY, WAPPALYZER_SIGNAL_WEIGHT, } from './technology-signal-definitions.js';
2
+ /**
3
+ * Converts beholder's `TagsMeta.entries` (per-page Wappalyzer detections,
4
+ * one row per provider × external-id tuple) into `wappalyzer` technology
5
+ * signals.
6
+ *
7
+ * Every entry becomes a signal — Wappalyzer is not limited to the curated
8
+ * frameworks {@link TECHNOLOGY_SIGNAL_DEFINITIONS} tracks; analytics tools,
9
+ * CMSes, and everything else Wappalyzer's fingerprint DB knows about pass
10
+ * through as their own technology (provider name verbatim). Only the
11
+ * curated frameworks get their provider name FOLDED onto the same
12
+ * technology name their structural signals use, via
13
+ * {@link WAPPALYZER_PROVIDER_TO_TECHNOLOGY} — so e.g. Wappalyzer's `'Vue.js'`
14
+ * and a `data-v-*` structural match both contribute to the `'Vue'` row,
15
+ * not two separate rows.
16
+ *
17
+ * This is the direct replacement for the old `extract-tags-for-archive.ts`
18
+ * (which still exists, unmodified, for `scripts/migrate-to-0.10.mjs`'s
19
+ * independent use — see that file's JSDoc) in the LIVE crawl write path.
20
+ * @param entries - `meta.tags.entries` from beholder, or `undefined`
21
+ * (legacy minimal-`Meta` test fixtures predate the required field).
22
+ * @returns One signal per entry; empty array when no tags were detected.
23
+ */
24
+ export function normalizeWappalyzerEntries(entries) {
25
+ if (!entries || entries.length === 0)
26
+ return [];
27
+ return entries.map((entry) => ({
28
+ technology: WAPPALYZER_PROVIDER_TO_TECHNOLOGY[entry.provider] ?? entry.provider,
29
+ signalType: 'wappalyzer',
30
+ evidence: entry.id ?? entry.provider,
31
+ weight: entry.confidence ?? WAPPALYZER_SIGNAL_WEIGHT,
32
+ category: entry.categories[0] ?? null,
33
+ version: entry.version ?? null,
34
+ }));
35
+ }
@@ -0,0 +1,43 @@
1
+ import type { TechnologySignalDefinition } from './types.js';
2
+ /**
3
+ * Fixed fingerprint list for the frontend frameworks this module detects via
4
+ * structural signals (URL path fragments, HTML markers, scoped-attribute
5
+ * conventions). Deliberately a TypeScript array, not an external config
6
+ * file (per the accepted design: precision/coverage is refined by editing
7
+ * this file, not by runtime configuration).
8
+ *
9
+ * Every framework here is ALSO detectable via {@link WAPPALYZER_PROVIDER_TO_TECHNOLOGY}
10
+ * when beholder's Wappalyzer pass independently reports it — the two signal
11
+ * sources compound in `combineTechnologyConfidence` rather than one
12
+ * replacing the other. Wappalyzer detects many more technologies than the
13
+ * frameworks listed here (analytics, CMSes, ...); those pass through with
14
+ * the provider name verbatim as the technology name, uncurated.
15
+ * @module
16
+ */
17
+ export declare const TECHNOLOGY_SIGNAL_DEFINITIONS: readonly TechnologySignalDefinition[];
18
+ /**
19
+ * `<meta name="generator">` prefix matchers, checked case-insensitively
20
+ * against `page_meta.generator` (already extracted — no new HTML scan
21
+ * needed). Distinct from {@link TECHNOLOGY_SIGNAL_DEFINITIONS} because the
22
+ * match strategy is prefix-on-a-known-field, not regex-over-HTML.
23
+ */
24
+ export declare const GENERATOR_TECHNOLOGY_MATCHERS: ReadonlyArray<{
25
+ technology: string;
26
+ prefix: string;
27
+ weight: number;
28
+ category: string;
29
+ }>;
30
+ /**
31
+ * Wappalyzer provider name → normalized technology name, for the curated
32
+ * frameworks that also have {@link TECHNOLOGY_SIGNAL_DEFINITIONS}. Providers
33
+ * NOT in this map still become `page_technologies` rows — the technology
34
+ * name is just the provider name verbatim (see `normalize-wappalyzer-entries.ts`)
35
+ * — this map exists only to fold curated frameworks' Wappalyzer detections
36
+ * onto the same technology name their structural signals use, so the two
37
+ * sources compound instead of appearing as two separate rows.
38
+ */
39
+ export declare const WAPPALYZER_PROVIDER_TO_TECHNOLOGY: Readonly<Record<string, string>>;
40
+ /** Confidence a lone `wappalyzer` signal contributes, before compounding with any structural signal. */
41
+ export declare const WAPPALYZER_SIGNAL_WEIGHT = 60;
42
+ /** Confidence threshold (`combineTechnologyConfidence`'s output) at or above which a `page_technologies` row is written at all. Below this, a single weak/ambiguous signal is treated as noise, not a detection. */
43
+ export declare const TECHNOLOGY_CONFIDENCE_THRESHOLD = 30;