@detiq/crawler 1.1.7 → 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 (92) 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.d.ts +10 -0
  22. package/dist/extractors/advanced.js +266 -249
  23. package/dist/extractors/artifact-envelope.js +58 -59
  24. package/dist/failure-analysis-agent.js +33 -37
  25. package/dist/feature-flag-detector.js +66 -51
  26. package/dist/form-detector.js +75 -55
  27. package/dist/frontier.js +101 -76
  28. package/dist/gap-pass.js +89 -68
  29. package/dist/graphql-detector.js +72 -62
  30. package/dist/http-engine.js +330 -282
  31. package/dist/incremental-diff.js +87 -54
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +28 -63
  34. package/dist/index.js.map +2 -2
  35. package/dist/input-fuzzer.js +97 -93
  36. package/dist/interactive-probe.d.ts +36 -0
  37. package/dist/interactive-probe.js +827 -715
  38. package/dist/job-queue.js +2169 -2151
  39. package/dist/js-coverage.js +56 -52
  40. package/dist/js-router-detector.js +117 -86
  41. package/dist/language-detector.js +63 -51
  42. package/dist/link-classifier.js +50 -32
  43. package/dist/live-view.js +108 -91
  44. package/dist/locator-extractor.d.ts +37 -0
  45. package/dist/locator-extractor.js +199 -0
  46. package/dist/locator-extractor.js.map +7 -0
  47. package/dist/logger.js +7 -9
  48. package/dist/markdown-extract.js +61 -48
  49. package/dist/native-crawler.js +946 -780
  50. package/dist/network-observer.js +156 -135
  51. package/dist/network-throttle.js +40 -29
  52. package/dist/openapi-generator.js +92 -77
  53. package/dist/pdf-extractor.js +90 -72
  54. package/dist/performance-metrics.js +102 -80
  55. package/dist/phash.js +144 -125
  56. package/dist/pii-scrubber.js +68 -65
  57. package/dist/playwright-recorder.js +58 -70
  58. package/dist/policies/crawl-safety.js +34 -31
  59. package/dist/profiles/domain-profile.js +125 -130
  60. package/dist/proxy.js +172 -146
  61. package/dist/rate-limiter.js +65 -58
  62. package/dist/redirect-tracker.js +64 -60
  63. package/dist/redis-frontier.js +136 -116
  64. package/dist/redis-queue.js +144 -157
  65. package/dist/response-cache.js +140 -127
  66. package/dist/responsive-detector.js +46 -42
  67. package/dist/retry-policy.js +50 -46
  68. package/dist/robots-txt.js +83 -69
  69. package/dist/screen-reader.js +70 -45
  70. package/dist/screenshot-masker.js +77 -59
  71. package/dist/seo-checker.js +107 -88
  72. package/dist/session-capture.js +212 -192
  73. package/dist/session-export.js +133 -107
  74. package/dist/sitemap-writer.js +41 -38
  75. package/dist/sitemap.js +202 -141
  76. package/dist/skyvern.js +148 -123
  77. package/dist/sqs-queue.js +42 -42
  78. package/dist/sso-token.js +90 -74
  79. package/dist/stealth/human-behavior.js +173 -156
  80. package/dist/telemetry.js +82 -68
  81. package/dist/test-executor.js +335 -309
  82. package/dist/tls-fingerprint.js +88 -77
  83. package/dist/url-pattern-completer.js +90 -71
  84. package/dist/visual-diff-verdict.js +38 -23
  85. package/dist/webhook-notifier.js +69 -71
  86. package/dist/websocket-detector.js +68 -56
  87. package/dist/worker.js +38 -29
  88. package/package.json +1 -1
  89. package/src/index.ts +2 -0
  90. package/src/locator-extractor.ts +226 -0
  91. package/types/index.d.ts +2 -0
  92. package/types/locator-extractor.d.ts +37 -0
@@ -1,41 +1,37 @@
1
- import { readFileSync } from "node:fs";
2
- import { askTrackedWithImages, askTracked, deterministicSeed } from "@detiq/agents";
3
- import { AppBrain } from "@detiq/app-brain";
4
- async function analyzeFailure(ctx) {
5
- try {
6
- let llmCfg;
7
- if (ctx.tenantId) {
8
- llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, "testRunner").catch(() => void 0) ?? void 0;
9
- }
10
- if (!llmCfg) return null;
11
- const consoleSection = ctx.consoleLogs?.length ? `
12
-
13
- Browser console (last ${ctx.consoleLogs.length} entries):
14
- ${ctx.consoleLogs.map((l) => `[${l.type}] ${l.text}`).join("\n")}` : "";
15
- const networkSection = ctx.networkErrors?.length ? `
1
+ import { readFileSync } from 'node:fs';
2
+ import { askTrackedWithImages, askTracked, deterministicSeed } from '@detiq/agents';
3
+ import { AppBrain } from '@detiq/app-brain';
4
+ export async function analyzeFailure(ctx) {
5
+ try {
6
+ let llmCfg;
7
+ if (ctx.tenantId) {
8
+ llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, 'testRunner').catch(() => undefined) ?? undefined;
9
+ }
10
+ if (!llmCfg)
11
+ return null;
12
+ const consoleSection = ctx.consoleLogs?.length
13
+ ? `\n\nBrowser console (last ${ctx.consoleLogs.length} entries):\n${ctx.consoleLogs.map(l => `[${l.type}] ${l.text}`).join('\n')}`
14
+ : '';
15
+ const networkSection = ctx.networkErrors?.length
16
+ ? `\n\nFailed network requests:\n${ctx.networkErrors.map(n => `${n.method} ${n.url} → ${n.status}`).join('\n')}`
17
+ : '';
18
+ const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific — name the failing element, API endpoint, or error type. Do not suggest fixes.
16
19
 
17
- Failed network requests:
18
- ${ctx.networkErrors.map((n) => `${n.method} ${n.url} \u2192 ${n.status}`).join("\n")}` : "";
19
- const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific \u2014 name the failing element, API endpoint, or error type. Do not suggest fixes.
20
-
21
- Test step: ${ctx.stepDescription ?? "unknown"}
20
+ Test step: ${ctx.stepDescription ?? 'unknown'}
22
21
  Error: ${ctx.error}${consoleSection}${networkSection}`;
23
- if (ctx.screenshotPath) {
24
- try {
25
- const img = readFileSync(ctx.screenshotPath);
26
- const result2 = await askTrackedWithImages("", userText, [{ base64: img.toString("base64"), mediaType: "image/png" }], llmCfg);
27
- return result2.text || null;
28
- } catch {
29
- }
22
+ if (ctx.screenshotPath) {
23
+ try {
24
+ const img = readFileSync(ctx.screenshotPath);
25
+ const result = await askTrackedWithImages('', userText, [{ base64: img.toString('base64'), mediaType: 'image/png' }], llmCfg);
26
+ return result.text || null;
27
+ }
28
+ catch { /* fall through to text-only */ }
29
+ }
30
+ const failureSeed = deterministicSeed(`${ctx.tenantId ?? ''}:failure:${(ctx.stepDescription ?? '').slice(0, 100)}:${ctx.error.slice(0, 200)}`);
31
+ const result = await askTracked('', userText, llmCfg, 256, undefined, { temperature: 0, seed: failureSeed });
32
+ return result.text || null;
33
+ }
34
+ catch {
35
+ return null;
30
36
  }
31
- const failureSeed = deterministicSeed(`${ctx.tenantId ?? ""}:failure:${(ctx.stepDescription ?? "").slice(0, 100)}:${ctx.error.slice(0, 200)}`);
32
- const result = await askTracked("", userText, llmCfg, 256, void 0, { temperature: 0, seed: failureSeed });
33
- return result.text || null;
34
- } catch {
35
- return null;
36
- }
37
37
  }
38
- export {
39
- analyzeFailure
40
- };
41
- //# sourceMappingURL=failure-analysis-agent.js.map
@@ -1,55 +1,70 @@
1
+ /**
2
+ * Feature flag + A/B test detector.
3
+ *
4
+ * Detects popular feature flag / experiment SDKs loaded on the page:
5
+ * LaunchDarkly, Optimizely, Split.io, Unleash, GrowthBook, Statsig, Flagsmith, Amplitude Experiment.
6
+ *
7
+ * Also detects common A/B test patterns: data-testid attributes, gtm.js experiment layers,
8
+ * Optimizely variation IDs, VWO, etc.
9
+ *
10
+ * Why: ZeTa needs to know when a page shows different content to different users
11
+ * so test generation can account for variants and not generate flaky tests.
12
+ */
13
+ // SDK detectors — checked against window object + script URLs
1
14
  const FLAG_SDK_CHECKS = [
2
- { sdk: "LaunchDarkly", windowKeys: ["LDClient", "LaunchDarkly", "ldClient"], scriptPattern: /launchdarkly/i },
3
- { sdk: "Optimizely", windowKeys: ["optimizely", "optimizelySdk"], scriptPattern: /optimizely/i },
4
- { sdk: "Split.io", windowKeys: ["splitio", "SplitFactory"], scriptPattern: /split\.io/i },
5
- { sdk: "GrowthBook", windowKeys: ["growthbook", "GrowthBook"], scriptPattern: /growthbook/i },
6
- { sdk: "Statsig", windowKeys: ["statsig", "StatsigClient"], scriptPattern: /statsig/i },
7
- { sdk: "Flagsmith", windowKeys: ["flagsmith"], scriptPattern: /flagsmith/i },
8
- { sdk: "Unleash", windowKeys: ["unleash", "UnleashClient"], scriptPattern: /unleash/i },
9
- { sdk: "VWO", windowKeys: ["vwo", "_vwo"], scriptPattern: /vwo\.com/i },
10
- { sdk: "Google Optimize", windowKeys: ["gaData", "google_optimize"], scriptPattern: /googleoptimize/i },
11
- { sdk: "AB Tasty", windowKeys: ["ABTasty"], scriptPattern: /abtasty/i }
15
+ { sdk: 'LaunchDarkly', windowKeys: ['LDClient', 'LaunchDarkly', 'ldClient'], scriptPattern: /launchdarkly/i },
16
+ { sdk: 'Optimizely', windowKeys: ['optimizely', 'optimizelySdk'], scriptPattern: /optimizely/i },
17
+ { sdk: 'Split.io', windowKeys: ['splitio', 'SplitFactory'], scriptPattern: /split\.io/i },
18
+ { sdk: 'GrowthBook', windowKeys: ['growthbook', 'GrowthBook'], scriptPattern: /growthbook/i },
19
+ { sdk: 'Statsig', windowKeys: ['statsig', 'StatsigClient'], scriptPattern: /statsig/i },
20
+ { sdk: 'Flagsmith', windowKeys: ['flagsmith'], scriptPattern: /flagsmith/i },
21
+ { sdk: 'Unleash', windowKeys: ['unleash', 'UnleashClient'], scriptPattern: /unleash/i },
22
+ { sdk: 'VWO', windowKeys: ['vwo', '_vwo'], scriptPattern: /vwo\.com/i },
23
+ { sdk: 'Google Optimize', windowKeys: ['gaData', 'google_optimize'], scriptPattern: /googleoptimize/i },
24
+ { sdk: 'AB Tasty', windowKeys: ['ABTasty'], scriptPattern: /abtasty/i },
12
25
  ];
13
- async function detectFeatureFlags(page, url) {
14
- const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
15
- const result = await page.evaluate((checks) => {
16
- const flagSdks = [];
17
- const scripts = Array.from(document.querySelectorAll("script[src]")).map((s) => s.src ?? "");
18
- for (const check of checks) {
19
- let found = false;
20
- let confidence = "low";
21
- if (check.windowKeys?.some((k) => k in window)) {
22
- found = true;
23
- confidence = "high";
24
- }
25
- if (!found && check.scriptPattern) {
26
- const re = new RegExp(check.scriptPattern.source, "i");
27
- const matchingSrc = scripts.find((s) => re.test(s));
28
- if (matchingSrc) {
29
- found = true;
30
- confidence = "medium";
26
+ export async function detectFeatureFlags(page, url) {
27
+ const detectedAt = new Date().toISOString();
28
+ const result = await page.evaluate((checks) => {
29
+ const flagSdks = [];
30
+ const scripts = Array.from(document.querySelectorAll('script[src]')).map((s) => s.src ?? '');
31
+ for (const check of checks) {
32
+ let found = false;
33
+ let confidence = 'low';
34
+ // Check window keys
35
+ if (check.windowKeys?.some((k) => k in window)) {
36
+ found = true;
37
+ confidence = 'high';
38
+ }
39
+ // Check script URLs
40
+ if (!found && check.scriptPattern) {
41
+ const re = new RegExp(check.scriptPattern.source, 'i');
42
+ const matchingSrc = scripts.find(s => re.test(s));
43
+ if (matchingSrc) {
44
+ found = true;
45
+ confidence = 'medium';
46
+ }
47
+ }
48
+ if (found)
49
+ flagSdks.push({ sdk: check.sdk, confidence, details: undefined });
31
50
  }
32
- }
33
- if (found) flagSdks.push({ sdk: check.sdk, confidence, details: void 0 });
34
- }
35
- const abTools = [];
36
- if ("_vwo_code" in window || "_vis_opt_queue" in window) abTools.push({ tool: "VWO", experiments: [] });
37
- if ("dataLayer" in window) {
38
- const dl = window.dataLayer ?? [];
39
- const expEvents = dl.filter((e) => e?.event?.includes("experiment") || e?.experimentId);
40
- if (expEvents.length > 0) abTools.push({ tool: "Google Tag Manager Experiments", experiments: expEvents.slice(0, 5).map((e) => e.experimentId ?? "") });
41
- }
42
- return { flagSdks, abTools };
43
- }, FLAG_SDK_CHECKS).catch(() => ({ flagSdks: [], abTools: [] }));
44
- return {
45
- url,
46
- flagSdks: result.flagSdks,
47
- abTools: result.abTools,
48
- hasAnyFlags: result.flagSdks.length > 0 || result.abTools.length > 0,
49
- detectedAt
50
- };
51
+ // A/B tools
52
+ const abTools = [];
53
+ if ('_vwo_code' in window || '_vis_opt_queue' in window)
54
+ abTools.push({ tool: 'VWO', experiments: [] });
55
+ if ('dataLayer' in window) {
56
+ const dl = window.dataLayer ?? [];
57
+ const expEvents = dl.filter((e) => e?.event?.includes('experiment') || e?.experimentId);
58
+ if (expEvents.length > 0)
59
+ abTools.push({ tool: 'Google Tag Manager Experiments', experiments: expEvents.slice(0, 5).map((e) => e.experimentId ?? '') });
60
+ }
61
+ return { flagSdks, abTools };
62
+ }, FLAG_SDK_CHECKS).catch(() => ({ flagSdks: [], abTools: [] }));
63
+ return {
64
+ url,
65
+ flagSdks: result.flagSdks,
66
+ abTools: result.abTools,
67
+ hasAnyFlags: result.flagSdks.length > 0 || result.abTools.length > 0,
68
+ detectedAt,
69
+ };
51
70
  }
52
- export {
53
- detectFeatureFlags
54
- };
55
- //# sourceMappingURL=feature-flag-detector.js.map
@@ -1,58 +1,78 @@
1
+ /**
2
+ * Form detector — extract form metadata from crawled pages.
3
+ *
4
+ * For each form found on a page, captures: action URL, method, all input fields
5
+ * with their types/names/required/validation constraints, and submit button text.
6
+ *
7
+ * This data feeds ZeTa's test generator to create form validation test cases
8
+ * automatically (happy path + boundary/error cases per field type).
9
+ */
1
10
  function guessPurpose(fields, action) {
2
- const names = fields.map((f) => f.name?.toLowerCase() + (f.label ?? "").toLowerCase()).join(" ");
3
- if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return "login";
4
- if (/password/.test(names) && /confirm|repeat/.test(names)) return "registration";
5
- if (/search|query|q\b/.test(names) && fields.length <= 3) return "search";
6
- if (/card|payment|cvv|expir/.test(names)) return "checkout";
7
- if (/message|subject|contact/.test(names)) return "contact";
8
- if (/setting|preference|profile/.test(names)) return "settings";
9
- return "unknown";
11
+ const names = fields.map(f => f.name?.toLowerCase() + (f.label ?? '').toLowerCase()).join(' ');
12
+ if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4)
13
+ return 'login';
14
+ if (/password/.test(names) && /confirm|repeat/.test(names))
15
+ return 'registration';
16
+ if (/search|query|q\b/.test(names) && fields.length <= 3)
17
+ return 'search';
18
+ if (/card|payment|cvv|expir/.test(names))
19
+ return 'checkout';
20
+ if (/message|subject|contact/.test(names))
21
+ return 'contact';
22
+ if (/setting|preference|profile/.test(names))
23
+ return 'settings';
24
+ return 'unknown';
10
25
  }
11
- async function detectForms(page) {
12
- try {
13
- return await page.$$eval("form", (forms) => forms.map((form, index) => {
14
- const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
15
- const fields = [];
16
- for (const input of inputs) {
17
- const tag = input.tagName.toLowerCase();
18
- const type = tag === "button" ? "submit" : input.type || tag;
19
- if (["submit", "reset", "button", "hidden"].includes(type)) continue;
20
- let label = "";
21
- if (input.id) {
22
- const lbl = form.querySelector(`label[for="${input.id}"]`);
23
- if (lbl) label = (lbl.textContent ?? "").trim();
24
- }
25
- if (!label) {
26
- const parent = input.closest("label");
27
- if (parent) label = (parent.textContent ?? "").replace(input.value ?? "", "").trim().slice(0, 80);
28
- }
29
- const options = tag === "select" ? Array.from(input.options ?? []).map((o) => o.text?.trim()).filter(Boolean).slice(0, 20) : tag === "input" && type === "radio" ? [input.value] : void 0;
30
- fields.push({
31
- name: input.name || input.id || "",
32
- type,
33
- label: label || void 0,
34
- required: !!input.required,
35
- placeholder: input.placeholder || void 0,
36
- minLength: input.minLength > 0 ? input.minLength : void 0,
37
- maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : void 0,
38
- min: input.min || void 0,
39
- max: input.max || void 0,
40
- pattern: input.pattern || void 0,
41
- options,
42
- autocomplete: input.autocomplete || void 0
43
- });
44
- }
45
- const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]');
46
- const hasFileUpload = !!form.querySelector('input[type="file"]');
47
- const action = form.action || void 0;
48
- const method = (form.method || "get").toUpperCase();
49
- return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || void 0, hasFileUpload };
50
- })).then((forms) => forms.map((f) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
51
- } catch {
52
- return [];
53
- }
26
+ export async function detectForms(page) {
27
+ try {
28
+ return await page.$$eval('form', (forms) => forms.map((form, index) => {
29
+ const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
30
+ const fields = [];
31
+ for (const input of inputs) {
32
+ const tag = input.tagName.toLowerCase();
33
+ const type = tag === 'button' ? 'submit' : (input.type || tag);
34
+ if (['submit', 'reset', 'button', 'hidden'].includes(type))
35
+ continue;
36
+ // Find associated label
37
+ let label = '';
38
+ if (input.id) {
39
+ const lbl = form.querySelector(`label[for="${input.id}"]`);
40
+ if (lbl)
41
+ label = (lbl.textContent ?? '').trim();
42
+ }
43
+ if (!label) {
44
+ const parent = input.closest('label');
45
+ if (parent)
46
+ label = (parent.textContent ?? '').replace(input.value ?? '', '').trim().slice(0, 80);
47
+ }
48
+ const options = tag === 'select'
49
+ ? Array.from(input.options ?? []).map((o) => o.text?.trim()).filter(Boolean).slice(0, 20)
50
+ : tag === 'input' && type === 'radio'
51
+ ? [input.value]
52
+ : undefined;
53
+ fields.push({
54
+ name: input.name || input.id || '',
55
+ type,
56
+ label: label || undefined,
57
+ required: !!input.required,
58
+ placeholder: input.placeholder || undefined,
59
+ minLength: input.minLength > 0 ? input.minLength : undefined,
60
+ maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : undefined,
61
+ min: input.min || undefined,
62
+ max: input.max || undefined,
63
+ pattern: input.pattern || undefined,
64
+ options,
65
+ autocomplete: input.autocomplete || undefined,
66
+ });
67
+ }
68
+ const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]');
69
+ const hasFileUpload = !!form.querySelector('input[type="file"]');
70
+ const action = form.action || undefined;
71
+ const method = (form.method || 'get').toUpperCase();
72
+ return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || undefined, hasFileUpload };
73
+ })).then(forms => forms.map((f) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
74
+ }
75
+ catch {
76
+ return [];
77
+ }
54
78
  }
55
- export {
56
- detectForms
57
- };
58
- //# sourceMappingURL=form-detector.js.map
package/dist/frontier.js CHANGED
@@ -1,83 +1,108 @@
1
+ /**
2
+ * Configurable crawl traversal strategy — matches crawl4AI's BFS/DFS/
3
+ * Best-First deep-crawl strategies (ZeTa previously had a single hardcoded
4
+ * FIFO queue, i.e. always BFS).
5
+ *
6
+ * Array-like on purpose (`length`, `shift()`, `push()`, `filter()`) so it
7
+ * drops into crawler.ts's existing queue call sites with a rename, not a
8
+ * rewrite — BFS's `shift()` behavior is byte-for-byte the same as the plain
9
+ * array it replaces, so BFS (the default) has zero behavior change.
10
+ */
11
+ /**
12
+ * Best-First scoring heuristic: shallower paths score higher (breadth-like
13
+ * bias toward top-level sections before deep sub-pages), boosted further by
14
+ * any user-supplied keyword appearing in the URL. Simple and transparent by
15
+ * design — crawl4AI's own default scorer is a similarly simple keyword-
16
+ * relevance heuristic, not a learned model.
17
+ */
1
18
  function scoreUrl(url, keywords) {
2
- try {
3
- const u = new URL(url);
4
- const segments = u.pathname.split("/").filter(Boolean);
5
- let score = -segments.length;
6
- if (keywords.length > 0) {
7
- const lower = url.toLowerCase();
8
- for (const kw of keywords) {
9
- if (kw && lower.includes(kw)) score += 10;
10
- }
19
+ try {
20
+ const u = new URL(url);
21
+ const segments = u.pathname.split('/').filter(Boolean);
22
+ let score = -segments.length;
23
+ if (keywords.length > 0) {
24
+ const lower = url.toLowerCase();
25
+ for (const kw of keywords) {
26
+ if (kw && lower.includes(kw))
27
+ score += 10;
28
+ }
29
+ }
30
+ return score;
31
+ }
32
+ catch {
33
+ return 0;
11
34
  }
12
- return score;
13
- } catch {
14
- return 0;
15
- }
16
35
  }
17
- class CrawlFrontier {
18
- items = [];
19
- strategy;
20
- keywords;
21
- urlFilter;
22
- constructor(strategy, seedUrls, keywords = [], urlFilter) {
23
- this.strategy = strategy;
24
- this.keywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
25
- this.urlFilter = urlFilter ? (() => {
26
- try {
27
- return new RegExp(urlFilter, "i");
28
- } catch {
29
- return null;
30
- }
31
- })() : null;
32
- for (const url of seedUrls) this.push(url);
33
- }
34
- get length() {
35
- return this.items.length;
36
- }
37
- /**
38
- * P3b: sparse-parent boost — if the parent page had fewer than 5 interactive
39
- * elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so
40
- * the frontier revisits sparse branches before deep-linked content.
41
- */
42
- push(url, pushOpts) {
43
- if (this.urlFilter && !this.urlFilter.test(url)) return;
44
- const sparseBoost = this.strategy === "BEST_FIRST" && pushOpts?.parentElements !== void 0 && pushOpts.parentElements < 5 ? 1 : 0;
45
- this.items.push({ url, score: this.strategy === "BEST_FIRST" ? scoreUrl(url, this.keywords) + sparseBoost : 0 });
46
- }
47
- /** Dequeues the next URL per this frontier's strategy. undefined if empty. */
48
- shift() {
49
- if (this.items.length === 0) return void 0;
50
- switch (this.strategy) {
51
- case "DFS":
52
- return this.items.pop().url;
53
- case "BEST_FIRST": {
54
- let bestIdx = 0;
55
- for (let i = 1; i < this.items.length; i++) {
56
- if (this.items[i].score > this.items[bestIdx].score) bestIdx = i;
36
+ export class CrawlFrontier {
37
+ items = [];
38
+ strategy;
39
+ keywords;
40
+ urlFilter;
41
+ constructor(strategy, seedUrls, keywords = [], urlFilter) {
42
+ this.strategy = strategy;
43
+ this.keywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
44
+ this.urlFilter = urlFilter
45
+ ? (() => { try {
46
+ return new RegExp(urlFilter, 'i');
47
+ }
48
+ catch {
49
+ return null;
50
+ } })()
51
+ : null;
52
+ for (const url of seedUrls)
53
+ this.push(url);
54
+ }
55
+ get length() {
56
+ return this.items.length;
57
+ }
58
+ /**
59
+ * P3b: sparse-parent boost — if the parent page had fewer than 5 interactive
60
+ * elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so
61
+ * the frontier revisits sparse branches before deep-linked content.
62
+ */
63
+ push(url, pushOpts) {
64
+ if (this.urlFilter && !this.urlFilter.test(url))
65
+ return;
66
+ const sparseBoost = this.strategy === 'BEST_FIRST' &&
67
+ pushOpts?.parentElements !== undefined &&
68
+ pushOpts.parentElements < 5
69
+ ? 1
70
+ : 0;
71
+ this.items.push({ url, score: this.strategy === 'BEST_FIRST' ? scoreUrl(url, this.keywords) + sparseBoost : 0 });
72
+ }
73
+ /** Dequeues the next URL per this frontier's strategy. undefined if empty. */
74
+ shift() {
75
+ if (this.items.length === 0)
76
+ return undefined;
77
+ switch (this.strategy) {
78
+ case 'DFS':
79
+ return this.items.pop().url;
80
+ case 'BEST_FIRST': {
81
+ let bestIdx = 0;
82
+ for (let i = 1; i < this.items.length; i++) {
83
+ if (this.items[i].score > this.items[bestIdx].score)
84
+ bestIdx = i;
85
+ }
86
+ return this.items.splice(bestIdx, 1)[0].url;
87
+ }
88
+ case 'BFS':
89
+ default:
90
+ return this.items.shift().url;
57
91
  }
58
- return this.items.splice(bestIdx, 1)[0].url;
59
- }
60
- case "BFS":
61
- default:
62
- return this.items.shift().url;
63
92
  }
64
- }
65
- filter(predicate) {
66
- return this.items.filter((e) => predicate(e.url)).map((e) => e.url);
67
- }
68
- /** Remove queued URLs not matching the regex pattern. Returns count removed. */
69
- filterByPattern(pattern) {
70
- try {
71
- const re = new RegExp(pattern, "i");
72
- const before = this.items.length;
73
- this.items = this.items.filter((e) => re.test(e.url));
74
- return before - this.items.length;
75
- } catch {
76
- return 0;
93
+ filter(predicate) {
94
+ return this.items.filter((e) => predicate(e.url)).map((e) => e.url);
95
+ }
96
+ /** Remove queued URLs not matching the regex pattern. Returns count removed. */
97
+ filterByPattern(pattern) {
98
+ try {
99
+ const re = new RegExp(pattern, 'i');
100
+ const before = this.items.length;
101
+ this.items = this.items.filter((e) => re.test(e.url));
102
+ return before - this.items.length;
103
+ }
104
+ catch {
105
+ return 0;
106
+ }
77
107
  }
78
- }
79
108
  }
80
- export {
81
- CrawlFrontier
82
- };
83
- //# sourceMappingURL=frontier.js.map