@detiq/crawler 1.1.8 → 1.1.9

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 (82) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.js +266 -249
  22. package/dist/extractors/artifact-envelope.js +58 -59
  23. package/dist/failure-analysis-agent.js +33 -37
  24. package/dist/feature-flag-detector.js +66 -51
  25. package/dist/form-detector.js +75 -55
  26. package/dist/frontier.js +101 -76
  27. package/dist/gap-pass.js +89 -68
  28. package/dist/graphql-detector.js +72 -62
  29. package/dist/http-engine.js +330 -282
  30. package/dist/incremental-diff.js +87 -54
  31. package/dist/index.js +28 -67
  32. package/dist/input-fuzzer.js +97 -93
  33. package/dist/interactive-probe.js +827 -715
  34. package/dist/job-queue.js +2169 -2151
  35. package/dist/js-coverage.js +56 -52
  36. package/dist/js-router-detector.js +117 -86
  37. package/dist/language-detector.js +63 -51
  38. package/dist/link-classifier.js +50 -32
  39. package/dist/live-view.js +108 -91
  40. package/dist/locator-extractor.js +187 -151
  41. package/dist/logger.js +7 -9
  42. package/dist/markdown-extract.js +61 -48
  43. package/dist/native-crawler.js +946 -780
  44. package/dist/network-observer.js +156 -135
  45. package/dist/network-throttle.js +40 -29
  46. package/dist/openapi-generator.js +92 -77
  47. package/dist/pdf-extractor.js +90 -72
  48. package/dist/performance-metrics.js +102 -80
  49. package/dist/phash.js +144 -125
  50. package/dist/pii-scrubber.js +68 -65
  51. package/dist/playwright-recorder.js +58 -70
  52. package/dist/policies/crawl-safety.js +34 -31
  53. package/dist/profiles/domain-profile.js +125 -130
  54. package/dist/proxy.js +172 -146
  55. package/dist/rate-limiter.js +65 -58
  56. package/dist/redirect-tracker.js +64 -60
  57. package/dist/redis-frontier.js +136 -116
  58. package/dist/redis-queue.js +144 -157
  59. package/dist/response-cache.js +140 -127
  60. package/dist/responsive-detector.js +46 -42
  61. package/dist/retry-policy.js +50 -46
  62. package/dist/robots-txt.js +83 -69
  63. package/dist/screen-reader.js +70 -45
  64. package/dist/screenshot-masker.js +77 -59
  65. package/dist/seo-checker.js +107 -88
  66. package/dist/session-capture.js +212 -192
  67. package/dist/session-export.js +133 -107
  68. package/dist/sitemap-writer.js +41 -38
  69. package/dist/sitemap.js +202 -141
  70. package/dist/skyvern.js +148 -123
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +90 -74
  73. package/dist/stealth/human-behavior.js +173 -156
  74. package/dist/telemetry.js +82 -68
  75. package/dist/test-executor.js +335 -309
  76. package/dist/tls-fingerprint.js +88 -77
  77. package/dist/url-pattern-completer.js +90 -71
  78. package/dist/visual-diff-verdict.js +38 -23
  79. package/dist/webhook-notifier.js +69 -71
  80. package/dist/websocket-detector.js +68 -56
  81. package/dist/worker.js +38 -29
  82. package/package.json +1 -1
@@ -1,78 +1,90 @@
1
- async function auditColorContrast(page, url) {
2
- const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
3
- const violations = await page.evaluate(() => {
4
- function parseRgb(color) {
5
- const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
6
- return m ? [+m[1], +m[2], +m[3], m[4] !== void 0 ? +m[4] : 1] : null;
7
- }
8
- function luminance([r, g, b]) {
9
- const toLinear = (c) => {
10
- const n = c / 255;
11
- return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
12
- };
13
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
14
- }
15
- function contrastRatio(fg, bg) {
16
- const l1 = luminance(fg);
17
- const l2 = luminance(bg);
18
- const lighter = Math.max(l1, l2);
19
- const darker = Math.min(l1, l2);
20
- return Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
21
- }
22
- const violations2 = [];
23
- const elements = Array.from(document.querySelectorAll("p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button"));
24
- let checked = 0;
25
- for (const el of elements) {
26
- if (checked >= 100) break;
27
- const text = (el.textContent ?? "").trim().slice(0, 60);
28
- if (!text) continue;
29
- const style = window.getComputedStyle(el);
30
- if (style.display === "none" || style.visibility === "hidden") continue;
31
- const fg = parseRgb(style.color);
32
- const bg = parseRgb(style.backgroundColor);
33
- if (!fg || !bg || bg[3] === 0) continue;
34
- const fontSize = parseFloat(style.fontSize);
35
- const fontWeight = style.fontWeight;
36
- const isLargeText = fontSize >= 18 || fontSize >= 14 && (fontWeight === "bold" || +fontWeight >= 700);
37
- const ratio = contrastRatio(fg, bg);
38
- const aaRequired = isLargeText ? 3 : 4.5;
39
- const aaaRequired = isLargeText ? 4.5 : 7;
40
- if (ratio < aaRequired) {
41
- violations2.push({
42
- selector: el.tagName.toLowerCase(),
43
- text: text.slice(0, 50),
44
- foreground: style.color,
45
- background: style.backgroundColor,
46
- ratio,
47
- required: aaRequired,
48
- level: "AA",
49
- isLargeText,
50
- severity: "fail"
51
- });
52
- } else if (ratio < aaaRequired) {
53
- violations2.push({
54
- selector: el.tagName.toLowerCase(),
55
- text: text.slice(0, 50),
56
- foreground: style.color,
57
- background: style.backgroundColor,
58
- ratio,
59
- required: aaaRequired,
60
- level: "AAA",
61
- isLargeText,
62
- severity: "warn"
63
- });
64
- } else {
65
- checked++;
66
- }
67
- }
68
- return violations2;
69
- }).catch(() => []);
70
- const passCount = Math.max(0, 100 - violations.length);
71
- const criticals = violations.filter((v) => v.severity === "fail").length;
72
- const score = Math.max(0, 100 - criticals * 15 - violations.filter((v) => v.severity === "warn").length * 3);
73
- return { url, violations, passCount, score, auditedAt };
1
+ /**
2
+ * WCAG color contrast checker.
3
+ *
4
+ * Evaluates text contrast ratios against backgrounds for all visible text elements.
5
+ * Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)
6
+ * or AAA (7:1 for normal, 4.5:1 for large text) requirements.
7
+ *
8
+ * Uses computedStyle to get actual rendered colors — accounts for CSS variables
9
+ * and inheritance.
10
+ */
11
+ export async function auditColorContrast(page, url) {
12
+ const auditedAt = new Date().toISOString();
13
+ const violations = await page.evaluate(() => {
14
+ function parseRgb(color) {
15
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
16
+ return m ? [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1] : null;
17
+ }
18
+ function luminance([r, g, b]) {
19
+ const toLinear = (c) => {
20
+ const n = c / 255;
21
+ return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
22
+ };
23
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
24
+ }
25
+ function contrastRatio(fg, bg) {
26
+ const l1 = luminance(fg);
27
+ const l2 = luminance(bg);
28
+ const lighter = Math.max(l1, l2);
29
+ const darker = Math.min(l1, l2);
30
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
31
+ }
32
+ const violations = [];
33
+ const elements = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button'));
34
+ let checked = 0;
35
+ for (const el of elements) {
36
+ if (checked >= 100)
37
+ break; // limit to avoid perf hit
38
+ const text = (el.textContent ?? '').trim().slice(0, 60);
39
+ if (!text)
40
+ continue;
41
+ const style = window.getComputedStyle(el);
42
+ if (style.display === 'none' || style.visibility === 'hidden')
43
+ continue;
44
+ const fg = parseRgb(style.color);
45
+ const bg = parseRgb(style.backgroundColor);
46
+ if (!fg || !bg || bg[3] === 0)
47
+ continue; // transparent bg — can't check
48
+ const fontSize = parseFloat(style.fontSize);
49
+ const fontWeight = style.fontWeight;
50
+ const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || +fontWeight >= 700));
51
+ const ratio = contrastRatio(fg, bg);
52
+ const aaRequired = isLargeText ? 3.0 : 4.5;
53
+ const aaaRequired = isLargeText ? 4.5 : 7.0;
54
+ if (ratio < aaRequired) {
55
+ violations.push({
56
+ selector: el.tagName.toLowerCase(),
57
+ text: text.slice(0, 50),
58
+ foreground: style.color,
59
+ background: style.backgroundColor,
60
+ ratio,
61
+ required: aaRequired,
62
+ level: 'AA',
63
+ isLargeText,
64
+ severity: 'fail',
65
+ });
66
+ }
67
+ else if (ratio < aaaRequired) {
68
+ violations.push({
69
+ selector: el.tagName.toLowerCase(),
70
+ text: text.slice(0, 50),
71
+ foreground: style.color,
72
+ background: style.backgroundColor,
73
+ ratio,
74
+ required: aaaRequired,
75
+ level: 'AAA',
76
+ isLargeText,
77
+ severity: 'warn',
78
+ });
79
+ }
80
+ else {
81
+ checked++;
82
+ }
83
+ }
84
+ return violations;
85
+ }).catch(() => []);
86
+ const passCount = Math.max(0, 100 - violations.length);
87
+ const criticals = violations.filter(v => v.severity === 'fail').length;
88
+ const score = Math.max(0, 100 - criticals * 15 - violations.filter(v => v.severity === 'warn').length * 3);
89
+ return { url, violations, passCount, score, auditedAt };
74
90
  }
75
- export {
76
- auditColorContrast
77
- };
78
- //# sourceMappingURL=color-contrast.js.map
@@ -1,152 +1,152 @@
1
- function estimateTokens(text) {
2
- return Math.ceil(text.length / 4);
1
+ export function estimateTokens(text) {
2
+ return Math.ceil(text.length / 4);
3
3
  }
4
- function chunkContent(text, opts) {
5
- const size = opts.chunkSize ?? 2e3;
6
- const charSize = size * 4;
7
- const overlap = opts.overlap ?? 200;
8
- const overlapChars = overlap * 4;
9
- switch (opts.strategy) {
10
- case "fixed":
11
- return fixedChunk(text, charSize);
12
- case "overlap":
13
- return overlapChunk(text, charSize, overlapChars);
14
- case "sentence":
15
- return sentenceChunk(text, charSize);
16
- case "paragraph":
17
- return paragraphChunk(text, charSize);
18
- case "markdown-heading":
19
- return markdownHeadingChunk(text, charSize);
20
- case "semantic":
21
- return semanticChunk(text, charSize);
22
- case "recursive":
23
- return recursiveChunk(text, charSize, opts.separators ?? ["\n\n", "\n", ". ", " "]);
24
- default:
25
- return fixedChunk(text, charSize);
26
- }
4
+ export function chunkContent(text, opts) {
5
+ const size = opts.chunkSize ?? 2000;
6
+ const charSize = size * 4;
7
+ const overlap = opts.overlap ?? 200;
8
+ const overlapChars = overlap * 4;
9
+ switch (opts.strategy) {
10
+ case 'fixed': return fixedChunk(text, charSize);
11
+ case 'overlap': return overlapChunk(text, charSize, overlapChars);
12
+ case 'sentence': return sentenceChunk(text, charSize);
13
+ case 'paragraph': return paragraphChunk(text, charSize);
14
+ case 'markdown-heading': return markdownHeadingChunk(text, charSize);
15
+ case 'semantic': return semanticChunk(text, charSize);
16
+ case 'recursive': return recursiveChunk(text, charSize, opts.separators ?? ['\n\n', '\n', '. ', ' ']);
17
+ default: return fixedChunk(text, charSize);
18
+ }
27
19
  }
28
20
  function makeChunk(text, index, startChar, heading) {
29
- return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
21
+ return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
30
22
  }
31
23
  function fixedChunk(text, charSize) {
32
- const chunks = [];
33
- let i = 0;
34
- while (i < text.length) {
35
- const slice = text.slice(i, i + charSize);
36
- chunks.push(makeChunk(slice, chunks.length, i));
37
- i += charSize;
38
- }
39
- return chunks;
24
+ const chunks = [];
25
+ let i = 0;
26
+ while (i < text.length) {
27
+ const slice = text.slice(i, i + charSize);
28
+ chunks.push(makeChunk(slice, chunks.length, i));
29
+ i += charSize;
30
+ }
31
+ return chunks;
40
32
  }
41
33
  function overlapChunk(text, charSize, overlapChars) {
42
- const chunks = [];
43
- let i = 0;
44
- while (i < text.length) {
45
- const slice = text.slice(i, i + charSize);
46
- chunks.push(makeChunk(slice, chunks.length, i));
47
- i += charSize - overlapChars;
48
- if (i <= 0) break;
49
- }
50
- return chunks;
34
+ const chunks = [];
35
+ let i = 0;
36
+ while (i < text.length) {
37
+ const slice = text.slice(i, i + charSize);
38
+ chunks.push(makeChunk(slice, chunks.length, i));
39
+ i += charSize - overlapChars;
40
+ if (i <= 0)
41
+ break;
42
+ }
43
+ return chunks;
51
44
  }
52
45
  function sentenceChunk(text, charSize) {
53
- const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
54
- return groupIntoChunks(sentences, charSize);
46
+ const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
47
+ return groupIntoChunks(sentences, charSize);
55
48
  }
56
49
  function paragraphChunk(text, charSize) {
57
- const paras = text.split(/\n\n+/).filter((p) => p.trim());
58
- return groupIntoChunks(paras, charSize);
50
+ const paras = text.split(/\n\n+/).filter(p => p.trim());
51
+ return groupIntoChunks(paras, charSize);
59
52
  }
60
53
  function markdownHeadingChunk(text, charSize) {
61
- const chunks = [];
62
- const headingRe = /^(#{1,3}\s.+)$/m;
63
- const sections = text.split(/(?=^#{1,3}\s)/m).filter((s) => s.trim());
64
- let pos = 0;
65
- for (const section of sections) {
66
- const headingMatch = section.match(headingRe);
67
- const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, "").trim() : void 0;
68
- if (section.length <= charSize) {
69
- chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
70
- } else {
71
- const sub = paragraphChunk(section, charSize);
72
- for (const s of sub) {
73
- chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
74
- }
54
+ const chunks = [];
55
+ const headingRe = /^(#{1,3}\s.+)$/m;
56
+ const sections = text.split(/(?=^#{1,3}\s)/m).filter(s => s.trim());
57
+ let pos = 0;
58
+ for (const section of sections) {
59
+ const headingMatch = section.match(headingRe);
60
+ const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, '').trim() : undefined;
61
+ if (section.length <= charSize) {
62
+ chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
63
+ }
64
+ else {
65
+ // Section too large — sub-chunk by paragraph
66
+ const sub = paragraphChunk(section, charSize);
67
+ for (const s of sub) {
68
+ chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
69
+ }
70
+ }
71
+ pos += section.length;
75
72
  }
76
- pos += section.length;
77
- }
78
- return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
73
+ return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
79
74
  }
80
75
  function semanticChunk(text, charSize) {
81
- const paras = text.split(/\n\n+/).filter((p) => p.trim());
82
- const merged = [];
83
- let current = "";
84
- for (const para of paras) {
85
- if (current.length + para.length + 2 <= charSize) {
86
- current = current ? current + "\n\n" + para : para;
87
- } else {
88
- if (current) merged.push(current);
89
- current = para;
76
+ // Like paragraph but merges short paragraphs with the next
77
+ const paras = text.split(/\n\n+/).filter(p => p.trim());
78
+ const merged = [];
79
+ let current = '';
80
+ for (const para of paras) {
81
+ if (current.length + para.length + 2 <= charSize) {
82
+ current = current ? current + '\n\n' + para : para;
83
+ }
84
+ else {
85
+ if (current)
86
+ merged.push(current);
87
+ current = para;
88
+ }
90
89
  }
91
- }
92
- if (current) merged.push(current);
93
- return groupIntoChunks(merged, charSize);
90
+ if (current)
91
+ merged.push(current);
92
+ return groupIntoChunks(merged, charSize);
94
93
  }
95
94
  function recursiveChunk(text, charSize, separators) {
96
- if (text.length <= charSize) return [makeChunk(text, 0, 0)];
97
- const [sep, ...rest] = separators;
98
- if (!sep) return fixedChunk(text, charSize);
99
- const parts = text.split(sep).filter((p) => p);
100
- const chunks = [];
101
- let pos = 0;
102
- for (const part of parts) {
103
- if (part.length <= charSize) {
104
- chunks.push(makeChunk(part, chunks.length, pos));
105
- } else {
106
- const sub = recursiveChunk(part, charSize, rest);
107
- for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
95
+ if (text.length <= charSize)
96
+ return [makeChunk(text, 0, 0)];
97
+ const [sep, ...rest] = separators;
98
+ if (!sep)
99
+ return fixedChunk(text, charSize);
100
+ const parts = text.split(sep).filter(p => p);
101
+ const chunks = [];
102
+ let pos = 0;
103
+ for (const part of parts) {
104
+ if (part.length <= charSize) {
105
+ chunks.push(makeChunk(part, chunks.length, pos));
106
+ }
107
+ else {
108
+ const sub = recursiveChunk(part, charSize, rest);
109
+ for (const s of sub)
110
+ chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
111
+ }
112
+ pos += part.length + sep.length;
108
113
  }
109
- pos += part.length + sep.length;
110
- }
111
- return chunks;
114
+ return chunks;
112
115
  }
113
116
  function groupIntoChunks(parts, charSize) {
114
- const chunks = [];
115
- let current = "";
116
- let startChar = 0;
117
- let pos = 0;
118
- for (const part of parts) {
119
- if (current.length + part.length + 1 > charSize && current) {
120
- chunks.push(makeChunk(current.trim(), chunks.length, startChar));
121
- startChar = pos;
122
- current = part;
123
- } else {
124
- current = current ? current + " " + part : part;
117
+ const chunks = [];
118
+ let current = '';
119
+ let startChar = 0;
120
+ let pos = 0;
121
+ for (const part of parts) {
122
+ if (current.length + part.length + 1 > charSize && current) {
123
+ chunks.push(makeChunk(current.trim(), chunks.length, startChar));
124
+ startChar = pos;
125
+ current = part;
126
+ }
127
+ else {
128
+ current = current ? current + ' ' + part : part;
129
+ }
130
+ pos += part.length + 1;
125
131
  }
126
- pos += part.length + 1;
127
- }
128
- if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
129
- return chunks;
132
+ if (current.trim())
133
+ chunks.push(makeChunk(current.trim(), chunks.length, startChar));
134
+ return chunks;
130
135
  }
131
- function mergeSmallChunks(chunks, minTokens) {
132
- const minChars = minTokens * 4;
133
- const result = [];
134
- let i = 0;
135
- while (i < chunks.length) {
136
- if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
137
- const merged = chunks[i].text + "\n\n" + chunks[i + 1].text;
138
- result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
139
- i += 2;
140
- } else {
141
- result.push({ ...chunks[i], index: result.length });
142
- i++;
136
+ export function mergeSmallChunks(chunks, minTokens) {
137
+ const minChars = minTokens * 4;
138
+ const result = [];
139
+ let i = 0;
140
+ while (i < chunks.length) {
141
+ if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
142
+ const merged = chunks[i].text + '\n\n' + chunks[i + 1].text;
143
+ result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
144
+ i += 2;
145
+ }
146
+ else {
147
+ result.push({ ...chunks[i], index: result.length });
148
+ i++;
149
+ }
143
150
  }
144
- }
145
- return result;
151
+ return result;
146
152
  }
147
- export {
148
- chunkContent,
149
- estimateTokens,
150
- mergeSmallChunks
151
- };
152
- //# sourceMappingURL=content-chunker.js.map
@@ -1,60 +1,77 @@
1
+ /**
2
+ * Content deduplication — detect near-duplicate pages during crawl.
3
+ *
4
+ * Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),
5
+ * we use 8 structural fingerprints extracted from the markdown to detect pages that
6
+ * share the same layout/template even if the data differs.
7
+ *
8
+ * Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).
9
+ * These waste crawl budget and generate redundant test cases.
10
+ */
11
+ /** Extract structural fingerprints from markdown — layout-sensitive, data-insensitive */
1
12
  function extractFingerprints(markdown) {
2
- const lines = markdown.split("\n").filter((l) => l.trim().length > 0);
3
- const fps = [];
4
- const headings = lines.filter((l) => /^#{1,6} /.test(l));
5
- const hCounts = [0, 0, 0, 0, 0, 0];
6
- for (const h of headings) {
7
- const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
8
- hCounts[lvl - 1]++;
9
- }
10
- fps.push("h:" + hCounts.join(","));
11
- const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
12
- fps.push("lk:" + Math.floor(linkCount / 5));
13
- const listCount = lines.filter((l) => /^[\-\*\+] |^\d+\. /.test(l)).length;
14
- fps.push("li:" + Math.floor(listCount / 3));
15
- fps.push("code:" + (markdown.includes("```") ? "1" : "0"));
16
- fps.push("tbl:" + (markdown.includes("|") ? "1" : "0"));
17
- const paraCount = markdown.split(/\n{2,}/).filter((p) => p.trim().length > 30).length;
18
- fps.push("p:" + Math.floor(paraCount / 2));
19
- const firstH = headings[0]?.replace(/^#{1,6} /, "").toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
20
- fps.push("fh:" + firstH.slice(0, 20));
21
- const wordCount = markdown.split(/\s+/).length;
22
- fps.push("wc:" + Math.floor(wordCount / 50));
23
- return fps;
13
+ const lines = markdown.split('\n').filter(l => l.trim().length > 0);
14
+ const fps = [];
15
+ // FP1: heading structure (H1-H6 count distribution)
16
+ const headings = lines.filter(l => /^#{1,6} /.test(l));
17
+ const hCounts = [0, 0, 0, 0, 0, 0];
18
+ for (const h of headings) {
19
+ const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
20
+ hCounts[lvl - 1]++;
21
+ }
22
+ fps.push('h:' + hCounts.join(','));
23
+ // FP2: link count (grouped into buckets)
24
+ const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
25
+ fps.push('lk:' + Math.floor(linkCount / 5));
26
+ // FP3: list item count (grouped)
27
+ const listCount = lines.filter(l => /^[\-\*\+] |^\d+\. /.test(l)).length;
28
+ fps.push('li:' + Math.floor(listCount / 3));
29
+ // FP4: code block presence
30
+ fps.push('code:' + (markdown.includes('```') ? '1' : '0'));
31
+ // FP5: table presence
32
+ fps.push('tbl:' + (markdown.includes('|') ? '1' : '0'));
33
+ // FP6: approximate paragraph count
34
+ const paraCount = markdown.split(/\n{2,}/).filter(p => p.trim().length > 30).length;
35
+ fps.push('p:' + Math.floor(paraCount / 2));
36
+ // FP7: first heading text (normalized)
37
+ const firstH = headings[0]?.replace(/^#{1,6} /, '').toLowerCase().replace(/[^a-z0-9]/g, '') ?? '';
38
+ fps.push('fh:' + firstH.slice(0, 20));
39
+ // FP8: word count bucket
40
+ const wordCount = markdown.split(/\s+/).length;
41
+ fps.push('wc:' + Math.floor(wordCount / 50));
42
+ return fps;
24
43
  }
25
44
  function similarity(a, b) {
26
- if (a.length !== b.length) return 0;
27
- let matches = 0;
28
- for (let i = 0; i < a.length; i++) {
29
- if (a[i] === b[i]) matches++;
30
- }
31
- return matches / a.length;
45
+ if (a.length !== b.length)
46
+ return 0;
47
+ let matches = 0;
48
+ for (let i = 0; i < a.length; i++) {
49
+ if (a[i] === b[i])
50
+ matches++;
51
+ }
52
+ return matches / a.length;
32
53
  }
33
- class ContentDeduplicator {
34
- seen = /* @__PURE__ */ new Map();
35
- threshold;
36
- constructor(threshold = 0.85) {
37
- this.threshold = threshold;
38
- }
39
- check(url, markdown) {
40
- const fps = extractFingerprints(markdown);
41
- for (const [key, entry] of this.seen) {
42
- const score = similarity(fps, entry.fps);
43
- if (score >= this.threshold) {
44
- return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
45
- }
54
+ export class ContentDeduplicator {
55
+ seen = new Map();
56
+ threshold;
57
+ constructor(threshold = 0.85) {
58
+ this.threshold = threshold;
59
+ }
60
+ check(url, markdown) {
61
+ const fps = extractFingerprints(markdown);
62
+ for (const [key, entry] of this.seen) {
63
+ const score = similarity(fps, entry.fps);
64
+ if (score >= this.threshold) {
65
+ return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
66
+ }
67
+ }
68
+ this.seen.set(url, { fps, url });
69
+ return { isDuplicate: false, similarityScore: 1.0 };
70
+ }
71
+ size() {
72
+ return this.seen.size;
73
+ }
74
+ reset() {
75
+ this.seen.clear();
46
76
  }
47
- this.seen.set(url, { fps, url });
48
- return { isDuplicate: false, similarityScore: 1 };
49
- }
50
- size() {
51
- return this.seen.size;
52
- }
53
- reset() {
54
- this.seen.clear();
55
- }
56
77
  }
57
- export {
58
- ContentDeduplicator
59
- };
60
- //# sourceMappingURL=content-dedup.js.map