@detiq/crawler 1.1.9 → 1.1.11

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 (83) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
  83. package/src/job-queue.ts +136 -0
@@ -1,72 +1,60 @@
1
- /**
2
- * WebSocket detection — intercepts WS connections opened during page crawl.
3
- * Records connection URLs, message schemas, and message counts.
4
- * This lets ZeTa discover real-time API contracts (chat, notifications, presence)
5
- * that would be invisible to HTTP-only crawlers.
6
- */
7
1
  function inferProtocol(sample) {
8
- try {
9
- const obj = JSON.parse(sample);
10
- if (obj?.jsonrpc)
11
- return 'json-rpc';
12
- if (obj?.type === 'connection_init' || obj?.type === 'subscribe')
13
- return 'graphql-ws';
14
- if (typeof obj?.sid === 'string' || sample.startsWith('0{'))
15
- return 'socket.io';
16
- return 'json';
17
- }
18
- catch {
19
- return 'raw';
20
- }
2
+ try {
3
+ const obj = JSON.parse(sample);
4
+ if (obj?.jsonrpc) return "json-rpc";
5
+ if (obj?.type === "connection_init" || obj?.type === "subscribe") return "graphql-ws";
6
+ if (typeof obj?.sid === "string" || sample.startsWith("0{")) return "socket.io";
7
+ return "json";
8
+ } catch {
9
+ return "raw";
10
+ }
21
11
  }
22
- export function attachWebSocketDetector(cdpSession) {
23
- const connections = new Map();
24
- const onCreated = (evt) => {
25
- const { requestId, url, timestamp } = evt ?? {};
26
- if (!requestId || !url)
27
- return;
28
- connections.set(requestId, {
29
- url,
30
- framesSent: 0,
31
- framesReceived: 0,
32
- samples: [],
33
- protocol: 'unknown',
34
- openedAt: new Date(timestamp * 1000).toISOString(),
35
- });
36
- };
37
- const onFrame = (evt, direction) => {
38
- const { requestId, response } = evt ?? {};
39
- if (!requestId)
40
- return;
41
- const conn = connections.get(requestId);
42
- if (!conn)
43
- return;
44
- if (direction === 'sent')
45
- conn.framesSent++;
46
- else
47
- conn.framesReceived++;
48
- if (conn.samples.length < 10 && response?.payloadData) {
49
- const payload = String(response.payloadData).slice(0, 500);
50
- conn.samples.push({ direction, payload, ts: new Date().toISOString() });
51
- if (conn.protocol === 'unknown')
52
- conn.protocol = inferProtocol(payload);
53
- }
54
- };
55
- const onSent = (evt) => onFrame(evt, 'sent');
56
- const onReceived = (evt) => onFrame(evt, 'received');
57
- if (cdpSession?.on) {
58
- cdpSession.on('Network.webSocketCreated', onCreated);
59
- cdpSession.on('Network.webSocketFrameSent', onSent);
60
- cdpSession.on('Network.webSocketFrameReceived', onReceived);
12
+ function attachWebSocketDetector(cdpSession) {
13
+ const connections = /* @__PURE__ */ new Map();
14
+ const onCreated = (evt) => {
15
+ const { requestId, url, timestamp } = evt ?? {};
16
+ if (!requestId || !url) return;
17
+ connections.set(requestId, {
18
+ url,
19
+ framesSent: 0,
20
+ framesReceived: 0,
21
+ samples: [],
22
+ protocol: "unknown",
23
+ openedAt: new Date(timestamp * 1e3).toISOString()
24
+ });
25
+ };
26
+ const onFrame = (evt, direction) => {
27
+ const { requestId, response } = evt ?? {};
28
+ if (!requestId) return;
29
+ const conn = connections.get(requestId);
30
+ if (!conn) return;
31
+ if (direction === "sent") conn.framesSent++;
32
+ else conn.framesReceived++;
33
+ if (conn.samples.length < 10 && response?.payloadData) {
34
+ const payload = String(response.payloadData).slice(0, 500);
35
+ conn.samples.push({ direction, payload, ts: (/* @__PURE__ */ new Date()).toISOString() });
36
+ if (conn.protocol === "unknown") conn.protocol = inferProtocol(payload);
37
+ }
38
+ };
39
+ const onSent = (evt) => onFrame(evt, "sent");
40
+ const onReceived = (evt) => onFrame(evt, "received");
41
+ if (cdpSession?.on) {
42
+ cdpSession.on("Network.webSocketCreated", onCreated);
43
+ cdpSession.on("Network.webSocketFrameSent", onSent);
44
+ cdpSession.on("Network.webSocketFrameReceived", onReceived);
45
+ }
46
+ return {
47
+ getConnections: () => [...connections.values()],
48
+ detach() {
49
+ if (cdpSession?.off) {
50
+ cdpSession.off("Network.webSocketCreated", onCreated);
51
+ cdpSession.off("Network.webSocketFrameSent", onSent);
52
+ cdpSession.off("Network.webSocketFrameReceived", onReceived);
53
+ }
61
54
  }
62
- return {
63
- getConnections: () => [...connections.values()],
64
- detach() {
65
- if (cdpSession?.off) {
66
- cdpSession.off('Network.webSocketCreated', onCreated);
67
- cdpSession.off('Network.webSocketFrameSent', onSent);
68
- cdpSession.off('Network.webSocketFrameReceived', onReceived);
69
- }
70
- },
71
- };
55
+ };
72
56
  }
57
+ export {
58
+ attachWebSocketDetector
59
+ };
60
+ //# sourceMappingURL=websocket-detector.js.map
package/dist/worker.js CHANGED
@@ -1,42 +1,33 @@
1
- /**
2
- * Standalone worker process entrypoint for a dedicated crawler/worker container,
3
- * separate from the API server (see infrastructure/ecs-task-definitions/crawler.json).
4
- *
5
- * index.ts is a side-effect-free library barrel imported by @detiq/api — it must NOT
6
- * start any background loop itself, or importing a single helper from @detiq/crawler
7
- * would silently spin up a second worker inside the API process too. This file is
8
- * the actual entrypoint that keeps a dedicated worker container's process alive and
9
- * consuming jobs: `node dist/worker.js`.
10
- */
11
- import { startJobWorkerLoop, processDataRetentionPurge } from './job-queue.js';
12
- import { logger } from './logger.js';
1
+ import { startJobWorkerLoop, processDataRetentionPurge } from "./job-queue.js";
2
+ import { logger } from "./logger.js";
13
3
  async function main() {
14
- logger.info('[worker] starting standalone crawler worker process');
15
- // Durable DB-backed queue — claims every non-CRAWL job type, and CRAWL too when
16
- // JOB_QUEUE_BACKEND isn't 'sqs'. Safe to run in many worker replicas at once.
17
- startJobWorkerLoop();
18
- // Daily data-retention purge: runs immediately on startup then every 24 h.
19
- // Only runs in the dedicated worker container (this file), not in the API
20
- // process, so there is exactly one purge sweep regardless of API replica count.
21
- const runPurge = () => {
22
- processDataRetentionPurge().catch((err) => {
23
- logger.error({ err: String(err) }, '[worker] data-retention purge failed');
24
- });
25
- };
26
- runPurge();
27
- setInterval(runPurge, 24 * 60 * 60 * 1000);
28
- // Optional: also consume the SQS transport for horizontally-scaled CRAWL fleets.
29
- if (process.env.JOB_QUEUE_BACKEND === 'sqs') {
30
- const { startSqsWorker } = await import('./sqs-queue.js');
31
- startSqsWorker().catch((err) => {
32
- logger.error({ err: String(err) }, '[worker] SQS worker crashed');
33
- process.exit(1);
34
- });
35
- }
4
+ logger.info("[worker] starting standalone crawler worker process");
5
+ startJobWorkerLoop();
6
+ const runPurge = () => {
7
+ processDataRetentionPurge().catch((err) => {
8
+ logger.error({ err: String(err) }, "[worker] data-retention purge failed");
9
+ });
10
+ };
11
+ runPurge();
12
+ setInterval(runPurge, 24 * 60 * 60 * 1e3);
13
+ if (process.env.JOB_QUEUE_BACKEND === "sqs") {
14
+ const { startSqsWorker } = await import("./sqs-queue.js");
15
+ startSqsWorker().catch((err) => {
16
+ logger.error({ err: String(err) }, "[worker] SQS worker crashed");
17
+ process.exit(1);
18
+ });
19
+ }
36
20
  }
37
- process.on('SIGTERM', () => { logger.info('[worker] SIGTERM received, exiting'); process.exit(0); });
38
- process.on('SIGINT', () => { logger.info('[worker] SIGINT received, exiting'); process.exit(0); });
21
+ process.on("SIGTERM", () => {
22
+ logger.info("[worker] SIGTERM received, exiting");
23
+ process.exit(0);
24
+ });
25
+ process.on("SIGINT", () => {
26
+ logger.info("[worker] SIGINT received, exiting");
27
+ process.exit(0);
28
+ });
39
29
  main().catch((err) => {
40
- logger.error({ err: String(err) }, '[worker] fatal startup error');
41
- process.exit(1);
30
+ logger.error({ err: String(err) }, "[worker] fatal startup error");
31
+ process.exit(1);
42
32
  });
33
+ //# sourceMappingURL=worker.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
package/src/job-queue.ts CHANGED
@@ -2333,6 +2333,126 @@ async function processRepoScanJob(jobId: string, config: any) {
2333
2333
  }
2334
2334
  }
2335
2335
 
2336
+ // Fetch and parse framework config files (playwright.config.ts, cypress.config.ts, tsconfig.json)
2337
+ // Fetch and parse framework config files for all supported frameworks.
2338
+ // Pure regex/JSON — no LLM. Results override LLM inference for path detection.
2339
+ // Covers: playwright, cypress, jest, pytest, junit/maven/gradle, selenium-java/py,
2340
+ // webdriverio, cucumber — all frameworks in DetectedFramework.
2341
+ const configPaths: {
2342
+ testDir?: string; fixturesFolder?: string; baseURL?: string;
2343
+ tsconfigPaths?: Record<string, string[]>; tsconfigBaseUrl?: string;
2344
+ javaTestSourceDir?: string; jestModuleNameMapper?: Record<string, string>;
2345
+ } = {};
2346
+ try {
2347
+ const pathSet = new Set(treeResult.paths as string[]);
2348
+ const lang = framework.includes('java') ? 'java'
2349
+ : (framework.includes('py') || framework === 'pytest') ? 'python' : 'js';
2350
+
2351
+ const configFilesToFetch: Array<{ path: string; type: string }> = [];
2352
+ const tryAdd = (candidates: string[], type: string) => {
2353
+ for (const c of candidates) { if (pathSet.has(c)) { configFilesToFetch.push({ path: c, type }); break; } }
2354
+ };
2355
+
2356
+ if (lang === 'js') {
2357
+ if (framework.startsWith('playwright')) tryAdd(['playwright.config.ts', 'playwright.config.js'], 'playwright');
2358
+ if (framework.startsWith('cypress')) tryAdd(['cypress.config.ts', 'cypress.config.js'], 'cypress');
2359
+ if (framework === 'jest-ts' || framework === 'jest-js') {
2360
+ tryAdd(['jest.config.ts', 'jest.config.js', 'jest.config.mjs'], 'jest');
2361
+ tryAdd(['package.json'], 'package-json');
2362
+ }
2363
+ if (framework === 'webdriverio') tryAdd(['wdio.conf.ts', 'wdio.conf.js', 'wdio.config.ts', 'wdio.config.js'], 'wdio');
2364
+ if (framework === 'cucumber' || framework === 'gherkin') {
2365
+ tryAdd(['.cucumber.js', '.cucumber.cjs', 'cucumber.json', '.cucumber.yaml', '.cucumber.yml'], 'cucumber');
2366
+ tryAdd(['package.json'], 'package-json');
2367
+ }
2368
+ tryAdd(['tsconfig.json'], 'tsconfig');
2369
+ } else if (lang === 'python') {
2370
+ tryAdd(['pytest.ini'], 'pytest-ini');
2371
+ tryAdd(['pyproject.toml'], 'pyproject-toml');
2372
+ tryAdd(['setup.cfg'], 'setup-cfg');
2373
+ } else if (lang === 'java') {
2374
+ tryAdd(['pom.xml'], 'pom-xml');
2375
+ tryAdd(['build.gradle.kts', 'build.gradle'], 'build-gradle');
2376
+ tryAdd(['testng.xml', 'src/test/resources/testng.xml'], 'testng-xml');
2377
+ }
2378
+
2379
+ // Inline parsers — mirror parseFrameworkConfig from @detiq/agents (kept inline to avoid build dep)
2380
+ const normDir = (s: string) => s.replace(/^\.\//, '').replace(/\/$/, '');
2381
+ const globRoot = (pattern: string): string | null => {
2382
+ const root = pattern.replace(/\/\*\*.*$/, '').replace(/\/\*[^*].*$/, '');
2383
+ return (root && !root.startsWith('*') && !root.startsWith('{')) ? normDir(root) : null;
2384
+ };
2385
+
2386
+ const fetched = await Promise.all(
2387
+ configFilesToFetch.map(async ({ path: cfgPath, type }) => {
2388
+ const content = await fetchFileContent(token, conn.repoFullName, cfgPath).catch(() => null);
2389
+ if (!content) return null;
2390
+ const r: typeof configPaths = {};
2391
+
2392
+ if (type === 'playwright') {
2393
+ const m = content.match(/\btestDir\s*:\s*['"`]([^'"`]+)['"`]/);
2394
+ if (m) r.testDir = normDir(m[1]);
2395
+ const bm = content.match(/\bbaseURL\s*:\s*['"`]([^'"`]+)['"`]/);
2396
+ if (bm) r.baseURL = bm[1];
2397
+ } else if (type === 'cypress') {
2398
+ const sm = content.match(/\bspecPattern\s*:\s*['"`]([^'"`]+)['"`]/);
2399
+ if (sm) { const gr = globRoot(sm[1]); if (gr) r.testDir = gr; }
2400
+ const fm = content.match(/\bfixturesFolder\s*:\s*['"`]([^'"`]+)['"`]/);
2401
+ if (fm) r.fixturesFolder = normDir(fm[1]);
2402
+ const bm = content.match(/\bbaseUrl\s*:\s*['"`]([^'"`]+)['"`]/);
2403
+ if (bm) r.baseURL = bm[1];
2404
+ } else if (type === 'jest') {
2405
+ const rm = content.match(/\broots\s*:\s*\[([^\]]+)\]/);
2406
+ if (rm) { const fr = rm[1].match(/['"`]([^'"`]+)['"`]/)?.[1]; if (fr) r.testDir = fr.replace(/<rootDir>\/?/, '').replace(/\/$/, '') || 'src'; }
2407
+ if (!r.testDir) { const tm = content.match(/\btestMatch\s*:\s*\[([^\]]+)\]/); if (tm) { const fp = tm[1].match(/['"`]([^'"`]+)['"`]/)?.[1]; if (fp) { const gr = globRoot(fp.replace(/<rootDir>\/?/, '')); if (gr) r.testDir = gr; } } }
2408
+ const mm = content.match(/\bmoduleNameMapper\s*:\s*\{([^}]+)\}/s);
2409
+ if (mm) { const map: Record<string, string> = {}; for (const [, k, v] of mm[1].matchAll(/['"`]([^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g)) map[k] = v; if (Object.keys(map).length) r.jestModuleNameMapper = map; }
2410
+ } else if (type === 'package-json') {
2411
+ try { const p = JSON.parse(content) as any; const j = p?.jest ?? {}; if (Array.isArray(j.roots) && j.roots[0]) r.testDir = String(j.roots[0]).replace(/<rootDir>\/?/, '').replace(/\/$/, '') || 'src'; if (!r.testDir && Array.isArray(j.testMatch) && j.testMatch[0]) { const gr = globRoot(String(j.testMatch[0]).replace(/<rootDir>\/?/, '')); if (gr) r.testDir = gr; } if (j.moduleNameMapper && typeof j.moduleNameMapper === 'object') r.jestModuleNameMapper = j.moduleNameMapper; } catch { /* ignore */ }
2412
+ } else if (type === 'tsconfig') {
2413
+ try { const stripped = content.replace(/\/\/[^\n]*/g, '').replace(/,(\s*[}\]])/g, '$1'); const p = JSON.parse(stripped) as any; const opts = p?.compilerOptions ?? {}; if (opts.paths && typeof opts.paths === 'object') r.tsconfigPaths = opts.paths; if (typeof opts.baseUrl === 'string') r.tsconfigBaseUrl = opts.baseUrl; } catch { /* ignore */ }
2414
+ } else if (type === 'pytest-ini') {
2415
+ const m = content.match(/^\s*testpaths\s*=\s*(.+)$/m);
2416
+ if (m) { const first = m[1].trim().split(/\s+/)[0]; if (first) r.testDir = normDir(first); }
2417
+ } else if (type === 'pyproject-toml') {
2418
+ const m = content.match(/testpaths\s*=\s*\[([^\]]+)\]/);
2419
+ if (m) { const first = m[1].match(/['"]([^'"]+)['"]/)?.[1]; if (first) r.testDir = normDir(first); }
2420
+ if (!r.testDir) { const m2 = content.match(/testpaths\s*=\s*['"]([^'"]+)['"]/); if (m2) r.testDir = normDir(m2[1]); }
2421
+ } else if (type === 'setup-cfg') {
2422
+ const m = content.match(/^\s*testpaths\s*=\s*(.+)$/m);
2423
+ if (m) { const first = m[1].trim().split(/[\s,]+/)[0]; if (first) r.testDir = normDir(first); }
2424
+ } else if (type === 'pom-xml') {
2425
+ const m = content.match(/<testSourceDirectory>\s*([^<]+)\s*<\/testSourceDirectory>/);
2426
+ if (m) r.javaTestSourceDir = normDir(m[1].trim());
2427
+ } else if (type === 'build-gradle') {
2428
+ const m = content.match(/srcDir[s]?\s*[=([]?\s*['"`]([^'"`]+)['"`]/) ?? content.match(/srcDir\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
2429
+ if (m) r.javaTestSourceDir = normDir(m[1]);
2430
+ } else if (type === 'wdio') {
2431
+ const sm = content.match(/\bspecs\s*:\s*\[([^\]]+)\]/);
2432
+ if (sm) { const fp = sm[1].match(/['"`]([^'"`]+)['"`]/)?.[1]; if (fp) { const gr = globRoot(fp); if (gr) r.testDir = gr; } }
2433
+ const bm = content.match(/\bbaseUrl\s*:\s*['"`]([^'"`]+)['"`]/);
2434
+ if (bm) r.baseURL = bm[1];
2435
+ } else if (type === 'cucumber') {
2436
+ const pm = content.match(/\bpaths\s*:\s*\[([^\]]+)\]/);
2437
+ if (pm) { const fp = pm[1].match(/['"`]([^'"`]+)['"`]/)?.[1]; if (fp) { const gr = globRoot(fp); if (gr) r.testDir = gr; } }
2438
+ if (!r.testDir) { try { const p = JSON.parse(content) as any; const paths = p?.default?.paths ?? p?.paths; if (Array.isArray(paths) && paths[0]) { const gr = globRoot(String(paths[0])); if (gr) r.testDir = gr; } } catch { /* not JSON */ } }
2439
+ }
2440
+ return r;
2441
+ })
2442
+ );
2443
+
2444
+ for (const parsed of fetched) {
2445
+ if (!parsed) continue;
2446
+ if (parsed.testDir && !configPaths.testDir) configPaths.testDir = parsed.testDir;
2447
+ if (parsed.fixturesFolder && !configPaths.fixturesFolder) configPaths.fixturesFolder = parsed.fixturesFolder;
2448
+ if (parsed.baseURL && !configPaths.baseURL) configPaths.baseURL = parsed.baseURL;
2449
+ if (parsed.tsconfigPaths && !configPaths.tsconfigPaths) configPaths.tsconfigPaths = parsed.tsconfigPaths;
2450
+ if (parsed.tsconfigBaseUrl && !configPaths.tsconfigBaseUrl) configPaths.tsconfigBaseUrl = parsed.tsconfigBaseUrl;
2451
+ if (parsed.javaTestSourceDir && !configPaths.javaTestSourceDir) configPaths.javaTestSourceDir = parsed.javaTestSourceDir;
2452
+ if (parsed.jestModuleNameMapper && !configPaths.jestModuleNameMapper) configPaths.jestModuleNameMapper = parsed.jestModuleNameMapper;
2453
+ }
2454
+ } catch { /* non-fatal */ }
2455
+
2336
2456
  // Extract style profile for aligned generation
2337
2457
  // For Java, include main source paths so LLM can detect page object location in src/main/java/
2338
2458
  // Fall back to existing styleProfile from DB if extraction fails or returns null.
@@ -2362,6 +2482,22 @@ async function processRepoScanJob(jobId: string, config: any) {
2362
2482
  } catch { /* non-fatal */ }
2363
2483
  }
2364
2484
 
2485
+ // Merge config file ground-truth into styleProfile (Priority 0 — highest, no LLM inference needed).
2486
+ // Java: javaTestSourceDir also sets testLocation (equivalent field for Java).
2487
+ if (styleProfile && Object.keys(configPaths).length > 0) {
2488
+ if (configPaths.testDir) styleProfile.configTestDir = configPaths.testDir;
2489
+ if (configPaths.javaTestSourceDir) {
2490
+ styleProfile.configTestDir = configPaths.javaTestSourceDir;
2491
+ // Also set testLocation if not already present from LLM scan
2492
+ if (!styleProfile.testLocation) styleProfile.testLocation = configPaths.javaTestSourceDir;
2493
+ }
2494
+ if (configPaths.fixturesFolder) styleProfile.configFixturesFolder = configPaths.fixturesFolder;
2495
+ if (configPaths.baseURL) styleProfile.configBaseURL = configPaths.baseURL;
2496
+ if (configPaths.tsconfigPaths) styleProfile.tsconfigPaths = configPaths.tsconfigPaths;
2497
+ if (configPaths.tsconfigBaseUrl) styleProfile.tsconfigBaseUrl = configPaths.tsconfigBaseUrl;
2498
+ if (configPaths.jestModuleNameMapper) styleProfile.jestModuleNameMapper = configPaths.jestModuleNameMapper;
2499
+ }
2500
+
2365
2501
  // Extract page object inventory — finds POM/page classes in non-test files
2366
2502
  // Fall back to existing inventory from DB if fetch fails.
2367
2503
  let pageObjectInventory: any[] = (conn.pageObjectInventory as any[]) ?? [];