@detiq/crawler 0.1.3

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 (412) hide show
  1. package/Dockerfile +49 -0
  2. package/README.md +11 -0
  3. package/dist/accessibility-auditor.d.ts +29 -0
  4. package/dist/accessibility-auditor.js +57 -0
  5. package/dist/accessibility-auditor.js.map +7 -0
  6. package/dist/ai-login.d.ts +32 -0
  7. package/dist/ai-login.js +177 -0
  8. package/dist/ai-login.js.map +7 -0
  9. package/dist/auth-token-rotator.d.ts +47 -0
  10. package/dist/auth-token-rotator.js +58 -0
  11. package/dist/auth-token-rotator.js.map +7 -0
  12. package/dist/auth.d.ts +20 -0
  13. package/dist/auth.js +329 -0
  14. package/dist/auth.js.map +7 -0
  15. package/dist/browser-extension.d.ts +37 -0
  16. package/dist/browser-extension.js +113 -0
  17. package/dist/browser-extension.js.map +7 -0
  18. package/dist/captcha-solver.d.ts +16 -0
  19. package/dist/captcha-solver.js +105 -0
  20. package/dist/captcha-solver.js.map +7 -0
  21. package/dist/color-contrast.d.ts +30 -0
  22. package/dist/color-contrast.js +78 -0
  23. package/dist/color-contrast.js.map +7 -0
  24. package/dist/content-chunker.d.ts +18 -0
  25. package/dist/content-chunker.js +152 -0
  26. package/dist/content-chunker.js.map +7 -0
  27. package/dist/content-dedup.d.ts +23 -0
  28. package/dist/content-dedup.js +60 -0
  29. package/dist/content-dedup.js.map +7 -0
  30. package/dist/cookie-consent.d.ts +25 -0
  31. package/dist/cookie-consent.js +78 -0
  32. package/dist/cookie-consent.js.map +7 -0
  33. package/dist/cookie-jar.d.ts +20 -0
  34. package/dist/cookie-jar.js +61 -0
  35. package/dist/cookie-jar.js.map +7 -0
  36. package/dist/crawler.d.ts +179 -0
  37. package/dist/crawler.js +3672 -0
  38. package/dist/crawler.js.map +7 -0
  39. package/dist/custom-js-injector.d.ts +42 -0
  40. package/dist/custom-js-injector.js +50 -0
  41. package/dist/custom-js-injector.js.map +7 -0
  42. package/dist/dark-mode-capture.d.ts +23 -0
  43. package/dist/dark-mode-capture.js +75 -0
  44. package/dist/dark-mode-capture.js.map +7 -0
  45. package/dist/dead-link-detector.d.ts +37 -0
  46. package/dist/dead-link-detector.js +50 -0
  47. package/dist/dead-link-detector.js.map +7 -0
  48. package/dist/element-hash.d.ts +9 -0
  49. package/dist/element-hash.js +9 -0
  50. package/dist/element-hash.js.map +7 -0
  51. package/dist/email-otp.d.ts +42 -0
  52. package/dist/email-otp.js +177 -0
  53. package/dist/email-otp.js.map +7 -0
  54. package/dist/engine-router.d.ts +47 -0
  55. package/dist/engine-router.js +94 -0
  56. package/dist/engine-router.js.map +7 -0
  57. package/dist/export/bulk-export.d.ts +26 -0
  58. package/dist/export/bulk-export.js +76 -0
  59. package/dist/export/bulk-export.js.map +7 -0
  60. package/dist/export/vector-export.d.ts +76 -0
  61. package/dist/export/vector-export.js +85 -0
  62. package/dist/export/vector-export.js.map +7 -0
  63. package/dist/extractors/advanced.d.ts +54 -0
  64. package/dist/extractors/advanced.js +245 -0
  65. package/dist/extractors/advanced.js.map +7 -0
  66. package/dist/extractors/artifact-envelope.d.ts +63 -0
  67. package/dist/extractors/artifact-envelope.js +63 -0
  68. package/dist/extractors/artifact-envelope.js.map +7 -0
  69. package/dist/failure-analysis-agent.d.ts +16 -0
  70. package/dist/failure-analysis-agent.js +40 -0
  71. package/dist/failure-analysis-agent.js.map +7 -0
  72. package/dist/feature-flag-detector.d.ts +30 -0
  73. package/dist/feature-flag-detector.js +55 -0
  74. package/dist/feature-flag-detector.js.map +7 -0
  75. package/dist/form-detector.d.ts +34 -0
  76. package/dist/form-detector.js +58 -0
  77. package/dist/form-detector.js.map +7 -0
  78. package/dist/frontier.d.ts +32 -0
  79. package/dist/frontier.js +83 -0
  80. package/dist/frontier.js.map +7 -0
  81. package/dist/gap-pass.d.ts +35 -0
  82. package/dist/gap-pass.js +81 -0
  83. package/dist/gap-pass.js.map +7 -0
  84. package/dist/graphql-detector.d.ts +39 -0
  85. package/dist/graphql-detector.js +86 -0
  86. package/dist/graphql-detector.js.map +7 -0
  87. package/dist/http-engine.d.ts +80 -0
  88. package/dist/http-engine.js +304 -0
  89. package/dist/http-engine.js.map +7 -0
  90. package/dist/incremental-diff.d.ts +59 -0
  91. package/dist/incremental-diff.js +61 -0
  92. package/dist/incremental-diff.js.map +7 -0
  93. package/dist/index.d.ts +35 -0
  94. package/dist/index.js +61 -0
  95. package/dist/index.js.map +7 -0
  96. package/dist/input-fuzzer.d.ts +30 -0
  97. package/dist/input-fuzzer.js +105 -0
  98. package/dist/input-fuzzer.js.map +7 -0
  99. package/dist/interactive-probe.d.ts +104 -0
  100. package/dist/interactive-probe.js +442 -0
  101. package/dist/interactive-probe.js.map +7 -0
  102. package/dist/job-queue.d.ts +171 -0
  103. package/dist/job-queue.js +2256 -0
  104. package/dist/job-queue.js.map +7 -0
  105. package/dist/js-coverage.d.ts +29 -0
  106. package/dist/js-coverage.js +56 -0
  107. package/dist/js-coverage.js.map +7 -0
  108. package/dist/js-router-detector.d.ts +17 -0
  109. package/dist/js-router-detector.js +90 -0
  110. package/dist/js-router-detector.js.map +7 -0
  111. package/dist/language-detector.d.ts +22 -0
  112. package/dist/language-detector.js +57 -0
  113. package/dist/language-detector.js.map +7 -0
  114. package/dist/link-classifier.d.ts +23 -0
  115. package/dist/link-classifier.js +40 -0
  116. package/dist/link-classifier.js.map +7 -0
  117. package/dist/live-view.d.ts +36 -0
  118. package/dist/live-view.js +100 -0
  119. package/dist/live-view.js.map +7 -0
  120. package/dist/logger.d.ts +2 -0
  121. package/dist/logger.js +10 -0
  122. package/dist/logger.js.map +7 -0
  123. package/dist/markdown-extract.d.ts +8 -0
  124. package/dist/markdown-extract.js +54 -0
  125. package/dist/markdown-extract.js.map +7 -0
  126. package/dist/native-crawler.d.ts +69 -0
  127. package/dist/native-crawler.js +839 -0
  128. package/dist/native-crawler.js.map +7 -0
  129. package/dist/network-observer.d.ts +49 -0
  130. package/dist/network-observer.js +149 -0
  131. package/dist/network-observer.js.map +7 -0
  132. package/dist/network-throttle.d.ts +25 -0
  133. package/dist/network-throttle.js +34 -0
  134. package/dist/network-throttle.js.map +7 -0
  135. package/dist/openapi-generator.d.ts +49 -0
  136. package/dist/openapi-generator.js +89 -0
  137. package/dist/openapi-generator.js.map +7 -0
  138. package/dist/pdf-extractor.d.ts +28 -0
  139. package/dist/pdf-extractor.js +81 -0
  140. package/dist/pdf-extractor.js.map +7 -0
  141. package/dist/performance-metrics.d.ts +53 -0
  142. package/dist/performance-metrics.js +83 -0
  143. package/dist/performance-metrics.js.map +7 -0
  144. package/dist/phash.d.ts +38 -0
  145. package/dist/phash.js +115 -0
  146. package/dist/phash.js.map +7 -0
  147. package/dist/pii-scrubber.d.ts +21 -0
  148. package/dist/pii-scrubber.js +75 -0
  149. package/dist/pii-scrubber.js.map +7 -0
  150. package/dist/playwright-recorder.d.ts +39 -0
  151. package/dist/playwright-recorder.js +123 -0
  152. package/dist/playwright-recorder.js.map +7 -0
  153. package/dist/policies/crawl-safety.d.ts +9 -0
  154. package/dist/policies/crawl-safety.js +37 -0
  155. package/dist/policies/crawl-safety.js.map +7 -0
  156. package/dist/profiles/domain-profile.d.ts +19 -0
  157. package/dist/profiles/domain-profile.js +140 -0
  158. package/dist/profiles/domain-profile.js.map +7 -0
  159. package/dist/proxy.d.ts +50 -0
  160. package/dist/proxy.js +165 -0
  161. package/dist/proxy.js.map +7 -0
  162. package/dist/rate-limiter.d.ts +31 -0
  163. package/dist/rate-limiter.js +64 -0
  164. package/dist/rate-limiter.js.map +7 -0
  165. package/dist/redirect-tracker.d.ts +37 -0
  166. package/dist/redirect-tracker.js +65 -0
  167. package/dist/redirect-tracker.js.map +7 -0
  168. package/dist/redis-frontier.d.ts +48 -0
  169. package/dist/redis-frontier.js +126 -0
  170. package/dist/redis-frontier.js.map +7 -0
  171. package/dist/redis-queue.d.ts +48 -0
  172. package/dist/redis-queue.js +165 -0
  173. package/dist/redis-queue.js.map +7 -0
  174. package/dist/response-cache.d.ts +38 -0
  175. package/dist/response-cache.js +140 -0
  176. package/dist/response-cache.js.map +7 -0
  177. package/dist/responsive-detector.d.ts +29 -0
  178. package/dist/responsive-detector.js +46 -0
  179. package/dist/responsive-detector.js.map +7 -0
  180. package/dist/retry-policy.d.ts +20 -0
  181. package/dist/retry-policy.js +54 -0
  182. package/dist/retry-policy.js.map +7 -0
  183. package/dist/robots-txt.d.ts +9 -0
  184. package/dist/robots-txt.js +77 -0
  185. package/dist/robots-txt.js.map +7 -0
  186. package/dist/screen-reader.d.ts +16 -0
  187. package/dist/screen-reader.js +46 -0
  188. package/dist/screen-reader.js.map +7 -0
  189. package/dist/screenshot-masker.d.ts +28 -0
  190. package/dist/screenshot-masker.js +62 -0
  191. package/dist/screenshot-masker.js.map +7 -0
  192. package/dist/seo-checker.d.ts +33 -0
  193. package/dist/seo-checker.js +91 -0
  194. package/dist/seo-checker.js.map +7 -0
  195. package/dist/session-capture.d.ts +35 -0
  196. package/dist/session-capture.js +267 -0
  197. package/dist/session-capture.js.map +7 -0
  198. package/dist/session-export.d.ts +49 -0
  199. package/dist/session-export.js +117 -0
  200. package/dist/session-export.js.map +7 -0
  201. package/dist/sitemap-writer.d.ts +21 -0
  202. package/dist/sitemap-writer.js +47 -0
  203. package/dist/sitemap-writer.js.map +7 -0
  204. package/dist/sitemap.d.ts +51 -0
  205. package/dist/sitemap.js +152 -0
  206. package/dist/sitemap.js.map +7 -0
  207. package/dist/skyvern.d.ts +56 -0
  208. package/dist/skyvern.js +129 -0
  209. package/dist/skyvern.js.map +7 -0
  210. package/dist/sqs-queue.d.ts +2 -0
  211. package/dist/sqs-queue.js +47 -0
  212. package/dist/sqs-queue.js.map +7 -0
  213. package/dist/sso-token.d.ts +29 -0
  214. package/dist/sso-token.js +76 -0
  215. package/dist/sso-token.js.map +7 -0
  216. package/dist/stealth/human-behavior.d.ts +20 -0
  217. package/dist/stealth/human-behavior.js +168 -0
  218. package/dist/stealth/human-behavior.js.map +7 -0
  219. package/dist/telemetry.d.ts +34 -0
  220. package/dist/telemetry.js +77 -0
  221. package/dist/telemetry.js.map +7 -0
  222. package/dist/test-executor.d.ts +67 -0
  223. package/dist/test-executor.js +311 -0
  224. package/dist/test-executor.js.map +7 -0
  225. package/dist/tls-fingerprint.d.ts +50 -0
  226. package/dist/tls-fingerprint.js +81 -0
  227. package/dist/tls-fingerprint.js.map +7 -0
  228. package/dist/url-pattern-completer.d.ts +28 -0
  229. package/dist/url-pattern-completer.js +87 -0
  230. package/dist/url-pattern-completer.js.map +7 -0
  231. package/dist/visual-diff-verdict.d.ts +40 -0
  232. package/dist/visual-diff-verdict.js +25 -0
  233. package/dist/visual-diff-verdict.js.map +7 -0
  234. package/dist/webhook-notifier.d.ts +30 -0
  235. package/dist/webhook-notifier.js +76 -0
  236. package/dist/webhook-notifier.js.map +7 -0
  237. package/dist/websocket-detector.d.ts +24 -0
  238. package/dist/websocket-detector.js +60 -0
  239. package/dist/websocket-detector.js.map +7 -0
  240. package/dist/worker.d.ts +1 -0
  241. package/dist/worker.js +33 -0
  242. package/dist/worker.js.map +7 -0
  243. package/package.json +45 -0
  244. package/src/accessibility-auditor.ts +102 -0
  245. package/src/ai-login.ts +218 -0
  246. package/src/auth-token-rotator.ts +89 -0
  247. package/src/auth.ts +396 -0
  248. package/src/browser-extension.ts +186 -0
  249. package/src/captcha-solver.ts +143 -0
  250. package/src/color-contrast.ts +116 -0
  251. package/src/content-chunker.ts +170 -0
  252. package/src/content-dedup.ts +99 -0
  253. package/src/cookie-consent.ts +106 -0
  254. package/src/cookie-jar.ts +91 -0
  255. package/src/crawler.ts +4305 -0
  256. package/src/custom-js-injector.ts +93 -0
  257. package/src/dark-mode-capture.ts +110 -0
  258. package/src/dead-link-detector.ts +76 -0
  259. package/src/element-hash.ts +16 -0
  260. package/src/email-otp.ts +252 -0
  261. package/src/engine-router.ts +141 -0
  262. package/src/export/bulk-export.ts +85 -0
  263. package/src/export/vector-export.ts +160 -0
  264. package/src/extractors/advanced.ts +283 -0
  265. package/src/extractors/artifact-envelope.ts +111 -0
  266. package/src/failure-analysis-agent.ts +49 -0
  267. package/src/feature-flag-detector.ts +97 -0
  268. package/src/form-detector.ts +103 -0
  269. package/src/frontier.test.ts +64 -0
  270. package/src/frontier.ts +112 -0
  271. package/src/gap-pass.ts +122 -0
  272. package/src/graphql-detector.ts +129 -0
  273. package/src/http-engine.ts +433 -0
  274. package/src/incremental-diff.ts +121 -0
  275. package/src/index.ts +35 -0
  276. package/src/input-fuzzer.ts +129 -0
  277. package/src/interactive-probe.ts +623 -0
  278. package/src/job-queue.test.ts +120 -0
  279. package/src/job-queue.ts +2701 -0
  280. package/src/js-coverage.ts +87 -0
  281. package/src/js-router-detector.ts +112 -0
  282. package/src/language-detector.ts +82 -0
  283. package/src/link-classifier.ts +65 -0
  284. package/src/live-view.ts +141 -0
  285. package/src/logger.ts +9 -0
  286. package/src/markdown-extract.test.ts +73 -0
  287. package/src/markdown-extract.ts +76 -0
  288. package/src/native-crawler.ts +1106 -0
  289. package/src/network-observer.ts +204 -0
  290. package/src/network-throttle.ts +69 -0
  291. package/src/openapi-generator.ts +135 -0
  292. package/src/pdf-extractor.ts +123 -0
  293. package/src/performance-metrics.ts +134 -0
  294. package/src/phash.ts +151 -0
  295. package/src/pii-scrubber.ts +99 -0
  296. package/src/playwright-recorder.ts +170 -0
  297. package/src/policies/crawl-safety.ts +46 -0
  298. package/src/profiles/domain-profile.ts +167 -0
  299. package/src/proxy.ts +238 -0
  300. package/src/rate-limiter.ts +87 -0
  301. package/src/redirect-tracker.ts +97 -0
  302. package/src/redis-frontier.ts +143 -0
  303. package/src/redis-queue.ts +208 -0
  304. package/src/response-cache.ts +150 -0
  305. package/src/responsive-detector.ts +92 -0
  306. package/src/retry-policy.ts +87 -0
  307. package/src/robots-txt.ts +96 -0
  308. package/src/screen-reader.ts +89 -0
  309. package/src/screenshot-masker.ts +98 -0
  310. package/src/seo-checker.ts +134 -0
  311. package/src/session-capture.ts +325 -0
  312. package/src/session-export.test.ts +75 -0
  313. package/src/session-export.ts +172 -0
  314. package/src/sitemap-writer.ts +65 -0
  315. package/src/sitemap.test.ts +131 -0
  316. package/src/sitemap.ts +224 -0
  317. package/src/skyvern.ts +199 -0
  318. package/src/sqs-queue.ts +49 -0
  319. package/src/sso-token.ts +120 -0
  320. package/src/stealth/human-behavior.ts +191 -0
  321. package/src/telemetry.ts +122 -0
  322. package/src/test-executor.test.ts +72 -0
  323. package/src/test-executor.ts +424 -0
  324. package/src/tls-fingerprint.ts +111 -0
  325. package/src/url-pattern-completer.ts +114 -0
  326. package/src/visual-diff-verdict.test.ts +80 -0
  327. package/src/visual-diff-verdict.ts +57 -0
  328. package/src/webhook-notifier.ts +109 -0
  329. package/src/websocket-detector.ts +83 -0
  330. package/src/worker.ts +48 -0
  331. package/tsconfig.json +13 -0
  332. package/types/accessibility-auditor.d.ts +29 -0
  333. package/types/ai-login.d.ts +32 -0
  334. package/types/auth-token-rotator.d.ts +47 -0
  335. package/types/auth.d.ts +20 -0
  336. package/types/browser-extension.d.ts +37 -0
  337. package/types/bull-worker.d.ts +6 -0
  338. package/types/captcha-solver.d.ts +16 -0
  339. package/types/color-contrast.d.ts +30 -0
  340. package/types/content-chunker.d.ts +18 -0
  341. package/types/content-dedup.d.ts +23 -0
  342. package/types/cookie-consent.d.ts +25 -0
  343. package/types/cookie-jar.d.ts +20 -0
  344. package/types/crawler.d.ts +179 -0
  345. package/types/custom-js-injector.d.ts +42 -0
  346. package/types/dark-mode-capture.d.ts +23 -0
  347. package/types/dead-link-detector.d.ts +37 -0
  348. package/types/element-hash.d.ts +9 -0
  349. package/types/email-otp.d.ts +42 -0
  350. package/types/engine-router.d.ts +47 -0
  351. package/types/export/bulk-export.d.ts +26 -0
  352. package/types/export/vector-export.d.ts +76 -0
  353. package/types/extractors/advanced.d.ts +54 -0
  354. package/types/extractors/artifact-envelope.d.ts +63 -0
  355. package/types/failure-analysis-agent.d.ts +16 -0
  356. package/types/feature-flag-detector.d.ts +30 -0
  357. package/types/form-detector.d.ts +34 -0
  358. package/types/frontier.d.ts +32 -0
  359. package/types/gap-pass.d.ts +35 -0
  360. package/types/graphql-detector.d.ts +39 -0
  361. package/types/http-engine.d.ts +80 -0
  362. package/types/incremental-diff.d.ts +59 -0
  363. package/types/index.d.ts +35 -0
  364. package/types/input-fuzzer.d.ts +30 -0
  365. package/types/interactive-probe.d.ts +104 -0
  366. package/types/job-queue.d.ts +171 -0
  367. package/types/js-coverage.d.ts +29 -0
  368. package/types/js-router-detector.d.ts +17 -0
  369. package/types/language-detector.d.ts +22 -0
  370. package/types/link-classifier.d.ts +23 -0
  371. package/types/live-view.d.ts +36 -0
  372. package/types/logger.d.ts +2 -0
  373. package/types/markdown-extract.d.ts +8 -0
  374. package/types/native-crawler.d.ts +69 -0
  375. package/types/network-observer.d.ts +49 -0
  376. package/types/network-throttle.d.ts +25 -0
  377. package/types/openapi-generator.d.ts +49 -0
  378. package/types/pdf-extractor.d.ts +28 -0
  379. package/types/performance-metrics.d.ts +53 -0
  380. package/types/phash.d.ts +38 -0
  381. package/types/pii-scrubber.d.ts +21 -0
  382. package/types/playwright-recorder.d.ts +39 -0
  383. package/types/policies/crawl-safety.d.ts +9 -0
  384. package/types/profiles/domain-profile.d.ts +19 -0
  385. package/types/proxy.d.ts +50 -0
  386. package/types/rate-limiter.d.ts +31 -0
  387. package/types/redirect-tracker.d.ts +37 -0
  388. package/types/redis-frontier.d.ts +48 -0
  389. package/types/redis-queue.d.ts +48 -0
  390. package/types/response-cache.d.ts +38 -0
  391. package/types/responsive-detector.d.ts +29 -0
  392. package/types/retry-policy.d.ts +20 -0
  393. package/types/robots-txt.d.ts +9 -0
  394. package/types/screen-reader.d.ts +16 -0
  395. package/types/screenshot-masker.d.ts +28 -0
  396. package/types/seo-checker.d.ts +33 -0
  397. package/types/session-capture.d.ts +38 -0
  398. package/types/session-export.d.ts +49 -0
  399. package/types/sitemap-writer.d.ts +21 -0
  400. package/types/sitemap.d.ts +51 -0
  401. package/types/skyvern.d.ts +56 -0
  402. package/types/sqs-queue.d.ts +2 -0
  403. package/types/sso-token.d.ts +29 -0
  404. package/types/stealth/human-behavior.d.ts +20 -0
  405. package/types/telemetry.d.ts +34 -0
  406. package/types/test-executor.d.ts +67 -0
  407. package/types/tls-fingerprint.d.ts +50 -0
  408. package/types/url-pattern-completer.d.ts +28 -0
  409. package/types/visual-diff-verdict.d.ts +40 -0
  410. package/types/webhook-notifier.d.ts +30 -0
  411. package/types/websocket-detector.d.ts +24 -0
  412. package/types/worker.d.ts +1 -0
package/src/phash.ts ADDED
@@ -0,0 +1,151 @@
1
+ import sharp from 'sharp';
2
+ import type { OverlayOptions } from 'sharp';
3
+
4
+ /**
5
+ * Composites neutral grey (128,128,128) over each ignoredRegion (fractional
6
+ * 0-1 coordinates) so masked content — timestamps, live counters, ads —
7
+ * contributes nothing to any downstream pixel comparison. Shared by every
8
+ * comparison path (whole-image hash, per-cell grid hash, AI vision) so a
9
+ * region masked once is actually ignored everywhere, not just in the
10
+ * cheapest check.
11
+ */
12
+ export async function applyMask(
13
+ imagePath: string,
14
+ ignoredRegions: Array<{ x: number; y: number; width: number; height: number }>,
15
+ ): Promise<Buffer> {
16
+ const meta = await sharp(imagePath).metadata();
17
+ const W = meta.width ?? 1200;
18
+ const H = meta.height ?? 800;
19
+
20
+ const overlays: OverlayOptions[] = await Promise.all(
21
+ ignoredRegions.map(async (r) => {
22
+ const rW = Math.max(1, Math.round(r.width * W));
23
+ const rH = Math.max(1, Math.round(r.height * H));
24
+ const buf = await sharp({
25
+ create: { width: rW, height: rH, channels: 3, background: { r: 128, g: 128, b: 128 } },
26
+ }).png().toBuffer();
27
+ return { input: buf, top: Math.round(r.y * H), left: Math.round(r.x * W) } as OverlayOptions;
28
+ }),
29
+ );
30
+
31
+ return sharp(imagePath).composite(overlays).png().toBuffer();
32
+ }
33
+
34
+ /**
35
+ * Compute an 8x8 average hash (aHash) of an image file, with optional mask overlays.
36
+ * ignoredRegions are fractional (0-1 range), converted to pixels here.
37
+ * Masked regions are filled with neutral grey (128,128,128) before hashing.
38
+ */
39
+ async function hashWithMasks(
40
+ imagePath: string,
41
+ ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
42
+ ): Promise<bigint> {
43
+ const pipeline = ignoredRegions.length > 0
44
+ ? sharp(await applyMask(imagePath, ignoredRegions))
45
+ : sharp(imagePath);
46
+
47
+ const buf = await pipeline.resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer();
48
+ const pixels = Array.from(buf);
49
+ const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
50
+ return pixels.reduce((h, p, i) => (p >= mean ? h | (1n << BigInt(63 - i)) : h), 0n);
51
+ }
52
+
53
+ function hammingDistance(a: bigint, b: bigint): number {
54
+ let xor = a ^ b;
55
+ let count = 0;
56
+ while (xor !== 0n) { count += Number(xor & 1n); xor >>= 1n; }
57
+ return count;
58
+ }
59
+
60
+ export async function screenshotDiffPercent(
61
+ pathA: string,
62
+ pathB: string,
63
+ ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
64
+ ): Promise<{ pHashDistance: number; changeLevel: 'IDENTICAL' | 'MINOR' | 'MODERATE' | 'SIGNIFICANT' }> {
65
+ const [hashA, hashB] = await Promise.all([
66
+ hashWithMasks(pathA, ignoredRegions),
67
+ hashWithMasks(pathB, ignoredRegions),
68
+ ]);
69
+ let xor = hashA ^ hashB;
70
+ let dist = 0;
71
+ while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }
72
+ const pct = parseFloat(((dist / 64) * 100).toFixed(1));
73
+ const changeLevel = pct === 0 ? 'IDENTICAL' : pct < 10 ? 'MINOR' : pct < 30 ? 'MODERATE' : 'SIGNIFICANT';
74
+ return { pHashDistance: pct, changeLevel };
75
+ }
76
+
77
+ /**
78
+ * Grid-based layout analysis: divides screenshots into a 3×3 grid and computes
79
+ * pHash per cell. Returns which regions changed and where, for the VisualDiff.changes field.
80
+ * Uses sharp's `extract` to crop each cell before hashing.
81
+ */
82
+ export async function gridLayoutDiff(
83
+ pathA: string,
84
+ pathB: string,
85
+ ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
86
+ ): Promise<Array<{ location: string; severity: string; description: string }>> {
87
+ const GRID_LABELS = [
88
+ 'top-left', 'top-center', 'top-right',
89
+ 'mid-left', 'mid-center', 'mid-right',
90
+ 'bottom-left', 'bottom-center', 'bottom-right',
91
+ ];
92
+
93
+ try {
94
+ // Mask before cropping into cells — a masked region (e.g. a timestamp)
95
+ // then contributes nothing to any cell's hash, instead of only being
96
+ // ignored by the separate whole-image hash in screenshotDiffPercent().
97
+ const [sourceA, sourceB] = ignoredRegions.length > 0
98
+ ? await Promise.all([applyMask(pathA, ignoredRegions), applyMask(pathB, ignoredRegions)])
99
+ : [pathA, pathB];
100
+
101
+ const [metaA, metaB] = await Promise.all([
102
+ sharp(sourceA).metadata(),
103
+ sharp(sourceB).metadata(),
104
+ ]);
105
+ const W = Math.min(metaA.width ?? 1200, metaB.width ?? 1200);
106
+ const H = Math.min(metaA.height ?? 800, metaB.height ?? 800);
107
+ const cW = Math.floor(W / 3);
108
+ const cH = Math.floor(H / 3);
109
+
110
+ const changes: Array<{ location: string; severity: string; description: string }> = [];
111
+
112
+ for (let row = 0; row < 3; row++) {
113
+ for (let col = 0; col < 3; col++) {
114
+ const region = { left: col * cW, top: row * cH, width: cW, height: cH };
115
+ const label = GRID_LABELS[row * 3 + col];
116
+
117
+ const [hashA, hashB] = await Promise.all([
118
+ sharp(sourceA).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()
119
+ .then((buf) => {
120
+ const pixels = Array.from(buf);
121
+ const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
122
+ return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);
123
+ }),
124
+ sharp(sourceB).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()
125
+ .then((buf) => {
126
+ const pixels = Array.from(buf);
127
+ const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
128
+ return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);
129
+ }),
130
+ ]);
131
+
132
+ let xor = hashA ^ hashB;
133
+ let dist = 0;
134
+ while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }
135
+ const pct = (dist / 64) * 100;
136
+
137
+ if (pct >= 5) {
138
+ const severity = pct >= 20 ? 'HIGH' : pct >= 10 ? 'MEDIUM' : 'LOW';
139
+ changes.push({
140
+ location: label,
141
+ severity,
142
+ description: `${Math.round(pct)}% visual change in ${label.replace('-', ' ')} region`,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ return changes;
148
+ } catch {
149
+ return [];
150
+ }
151
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * PII scrubber — detects and redacts Personally Identifiable Information
3
+ * from crawled markdown, HTML, and extracted text before storage.
4
+ *
5
+ * GDPR Article 4(1): "any information relating to an identified or identifiable natural person"
6
+ */
7
+
8
+ export interface PiiMatch {
9
+ type: 'email' | 'phone' | 'ssn' | 'credit-card' | 'ip-address' | 'passport' | 'iban' | 'dob';
10
+ value: string;
11
+ start: number;
12
+ end: number;
13
+ }
14
+
15
+ export interface ScrubResult {
16
+ text: string;
17
+ matches: PiiMatch[];
18
+ scrubbed: boolean;
19
+ }
20
+
21
+ // Patterns — ordered from most-specific to least-specific
22
+ const PATTERNS: Array<{ type: PiiMatch['type']; re: RegExp }> = [
23
+ // Email — RFC 5321 simplified
24
+ { type: 'email', re: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g },
25
+ // US SSN — 9 digits with dashes or spaces
26
+ { type: 'ssn', re: /\b(?!000|666|9\d\d)\d{3}[-\s](?!00)\d{2}[-\s](?!0000)\d{4}\b/g },
27
+ // Credit card — Visa/MC/Amex/Discover with optional spaces/dashes
28
+ { type: 'credit-card', re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{3,4}\b/g },
29
+ // IBAN — international bank account (15-34 chars)
30
+ { type: 'iban', re: /\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b/g },
31
+ // Phone — international or US formats
32
+ { type: 'phone', re: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b|\+\d{1,3}[-.\s]\d{2,4}[-.\s]\d{3,4}[-.\s]\d{3,4}/g },
33
+ // Date of birth — common formats
34
+ { type: 'dob', re: /\b(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[12]\d|3[01])\/(?:19|20)\d{2}\b/g },
35
+ // IPv4 private/internal addresses — only flag private-looking ones in content
36
+ { type: 'ip-address', re: /\b(?:192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b/g },
37
+ ];
38
+
39
+ const REDACT_MAP: Record<PiiMatch['type'], string> = {
40
+ email: '[EMAIL]',
41
+ phone: '[PHONE]',
42
+ ssn: '[SSN]',
43
+ 'credit-card': '[CARD]',
44
+ 'ip-address': '[IP]',
45
+ passport: '[PASSPORT]',
46
+ iban: '[IBAN]',
47
+ dob: '[DOB]',
48
+ };
49
+
50
+ export function detectPii(text: string): PiiMatch[] {
51
+ const matches: PiiMatch[] = [];
52
+ const seen = new Set<string>();
53
+
54
+ for (const { type, re } of PATTERNS) {
55
+ const r = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
56
+ let m: RegExpExecArray | null;
57
+ while ((m = r.exec(text)) !== null) {
58
+ const key = `${type}:${m.index}`;
59
+ if (!seen.has(key)) {
60
+ seen.add(key);
61
+ matches.push({ type, value: m[0], start: m.index, end: m.index + m[0].length });
62
+ }
63
+ }
64
+ }
65
+
66
+ return matches.sort((a, b) => a.start - b.start);
67
+ }
68
+
69
+ export function scrubPii(text: string): ScrubResult {
70
+ const matches = detectPii(text);
71
+ if (matches.length === 0) return { text, matches: [], scrubbed: false };
72
+
73
+ // Replace from end to start so indices stay valid
74
+ let scrubbed = text;
75
+ for (let i = matches.length - 1; i >= 0; i--) {
76
+ const { type, start, end } = matches[i];
77
+ scrubbed = scrubbed.slice(0, start) + REDACT_MAP[type] + scrubbed.slice(end);
78
+ }
79
+
80
+ return { text: scrubbed, matches, scrubbed: true };
81
+ }
82
+
83
+ export function hasPii(text: string): boolean {
84
+ for (const { re } of PATTERNS) {
85
+ const r = new RegExp(re.source, re.flags);
86
+ if (r.test(text)) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ export function scrubObject<T extends Record<string, unknown>>(obj: T, fields: (keyof T)[]): T {
92
+ const result = { ...obj };
93
+ for (const field of fields) {
94
+ if (typeof result[field] === 'string') {
95
+ (result as any)[field] = scrubPii(result[field] as string).text;
96
+ }
97
+ }
98
+ return result;
99
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Playwright test recorder — generate Playwright test code from crawl data.
3
+ *
4
+ * Given a list of crawled screens, generates a minimal Playwright test file that:
5
+ * 1. Navigates to each screen URL
6
+ * 2. Verifies the page title
7
+ * 3. Checks key elements visible on the page
8
+ * 4. Verifies no console errors (optional)
9
+ *
10
+ * This is a "smoke test" generator — not a full interaction recorder.
11
+ * Output: valid TypeScript Playwright test file ready to run with npx playwright test.
12
+ */
13
+
14
+ export interface RecorderScreen {
15
+ id: string;
16
+ name: string;
17
+ url: string;
18
+ title?: string;
19
+ heading?: string;
20
+ markdownWordCount?: number;
21
+ smartLocator?: {
22
+ tag: string; // e.g. 'button', 'a', 'input'
23
+ text?: string; // visible text content
24
+ role?: string; // ARIA role
25
+ placeholder?: string; // for inputs
26
+ nearText?: string; // nearby label text
27
+ };
28
+ }
29
+
30
+ export interface RecorderOpts {
31
+ baseUrl?: string;
32
+ includeConsoleCheck?: boolean;
33
+ testTimeout?: number; // ms, default 30000
34
+ projectName?: string;
35
+ useSmartLocators?: boolean; // default true
36
+ }
37
+
38
+ function buildSmartLocatorHelper(): string {
39
+ return `
40
+ // smartLocator — falls back to similarity search when primary selector fails.
41
+ // Generated by ZeTa Crawler. Do not edit manually.
42
+ async function smartLocator(page: any, primary: string, ref: { tag?: string; text?: string; role?: string; placeholder?: string }) {
43
+ try {
44
+ const el = page.locator(primary);
45
+ if (await el.count() > 0) return el;
46
+ } catch { /* fall through */ }
47
+ // Fallback: find by ARIA role + text
48
+ if (ref.role && ref.text) {
49
+ const byRole = page.getByRole(ref.role as any, { name: ref.text, exact: false });
50
+ if (await byRole.count() > 0) return byRole;
51
+ }
52
+ // Fallback: find by text content
53
+ if (ref.text) {
54
+ const byText = page.getByText(ref.text, { exact: false });
55
+ if (await byText.count() > 0) return byText;
56
+ }
57
+ // Fallback: find by placeholder
58
+ if (ref.placeholder) {
59
+ const byPlaceholder = page.getByPlaceholder(ref.placeholder, { exact: false });
60
+ if (await byPlaceholder.count() > 0) return byPlaceholder;
61
+ }
62
+ // Last resort: return the primary (will fail with original error)
63
+ return page.locator(primary);
64
+ }`.trim();
65
+ }
66
+
67
+ function sanitizeTestName(name: string): string {
68
+ return name.replace(/[^a-zA-Z0-9 _-]/g, '').trim().slice(0, 80) || 'Screen';
69
+ }
70
+
71
+ function escapeString(s: string): string {
72
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/`/g, '\\`');
73
+ }
74
+
75
+ export function generatePlaywrightTest(screens: RecorderScreen[], opts: RecorderOpts = {}): string {
76
+ const {
77
+ includeConsoleCheck = false,
78
+ testTimeout = 30_000,
79
+ projectName = 'ZeTa Crawl',
80
+ useSmartLocators = true,
81
+ } = opts;
82
+
83
+ const needsSmartLocator = useSmartLocators && screens.some(s => s.smartLocator != null);
84
+
85
+ const lines: string[] = [
86
+ `// Generated by ZeTa Crawler — ${new Date().toISOString()}`,
87
+ `// Project: ${escapeString(projectName)}`,
88
+ `// Screens: ${screens.length}`,
89
+ `// Run: npx playwright test`,
90
+ '',
91
+ "import { test, expect } from '@playwright/test';",
92
+ '',
93
+ ];
94
+
95
+ if (needsSmartLocator) {
96
+ lines.push(buildSmartLocatorHelper(), '');
97
+ }
98
+
99
+ lines.push(`test.setTimeout(${testTimeout});`, '');
100
+
101
+ if (includeConsoleCheck) {
102
+ lines.push(
103
+ `test.beforeEach(async ({ page }) => {`,
104
+ ` const errors: string[] = [];`,
105
+ ` page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); });`,
106
+ ` page.on('pageerror', err => errors.push(err.message));`,
107
+ ` (page as any).__consoleErrors = errors;`,
108
+ `});`,
109
+ '',
110
+ );
111
+ }
112
+
113
+ for (const screen of screens) {
114
+ const testName = sanitizeTestName(screen.name);
115
+ lines.push(
116
+ `test('${escapeString(testName)}', async ({ page }) => {`,
117
+ ` await page.goto('${escapeString(screen.url)}');`,
118
+ ` await expect(page).toHaveURL(/${escapeString(screen.url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))}/);`,
119
+ );
120
+
121
+ if (screen.title) {
122
+ lines.push(` await expect(page).toHaveTitle(/${escapeString(screen.title.slice(0, 40).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))}/i);`);
123
+ }
124
+
125
+ if (screen.heading) {
126
+ const h = escapeString(screen.heading.slice(0, 60));
127
+ if (needsSmartLocator && screen.smartLocator) {
128
+ const tag = escapeString(screen.smartLocator.tag);
129
+ const text = escapeString(screen.smartLocator.text ?? '');
130
+ lines.push(
131
+ ` const headingEl = await smartLocator(page, 'h1, h2, h3', { tag: '${tag}', text: '${text}' });`,
132
+ ` await expect(headingEl.first()).toContainText('${h}', { ignoreCase: true });`,
133
+ );
134
+ } else {
135
+ lines.push(` await expect(page.locator('h1, h2, h3').first()).toContainText('${h}', { ignoreCase: true });`);
136
+ }
137
+ }
138
+
139
+ if (includeConsoleCheck) {
140
+ lines.push(` expect((page as any).__consoleErrors ?? []).toHaveLength(0);`);
141
+ }
142
+
143
+ lines.push(`});`, '');
144
+ }
145
+
146
+ return lines.join('\n');
147
+ }
148
+
149
+ export function generatePlaywrightConfig(opts: { baseUrl?: string; reportDir?: string } = {}): string {
150
+ const { baseUrl = 'http://localhost:3000', reportDir = 'playwright-report' } = opts;
151
+ return `// playwright.config.ts — generated by ZeTa Crawler
152
+ import { defineConfig } from '@playwright/test';
153
+
154
+ export default defineConfig({
155
+ testDir: './tests',
156
+ reporter: [['html', { outputFolder: '${reportDir}' }]],
157
+ use: {
158
+ baseURL: '${baseUrl}',
159
+ headless: true,
160
+ screenshot: 'only-on-failure',
161
+ video: 'retain-on-failure',
162
+ },
163
+ projects: [
164
+ { name: 'chromium', use: { browserName: 'chromium' } },
165
+ { name: 'firefox', use: { browserName: 'firefox' } },
166
+ { name: 'webkit', use: { browserName: 'webkit' } },
167
+ ],
168
+ });
169
+ `;
170
+ }
@@ -0,0 +1,46 @@
1
+ import type { DomainProfile } from '../profiles/domain-profile.js';
2
+
3
+ export interface CrawlSafetyDecision {
4
+ allowed: boolean;
5
+ reason?: string;
6
+ severity: 'info' | 'warn' | 'block';
7
+ }
8
+
9
+ const DESTRUCTIVE_ACTION = /\b(delete|remove|destroy|deactivate|disable|cancel|refund|void|transfer|withdraw|pay now|place order|submit order|confirm payment|send money|publish|approve|reject|bulk update|drop)\b/i;
10
+ const SECRET_LIKE = /\b(api[_ -]?key|secret|token|password|authorization|cookie|set-cookie)\b/i;
11
+
12
+ export function assessActionSafety(label: string, profile: DomainProfile): CrawlSafetyDecision {
13
+ const riskyByProfile = profile.riskyActions.some((risk) => label.toLowerCase().includes(risk.toLowerCase()));
14
+ if (DESTRUCTIVE_ACTION.test(label) || riskyByProfile) {
15
+ return {
16
+ allowed: false,
17
+ severity: 'block',
18
+ reason: `Blocked risky action "${label}" for ${profile.industry} profile.`,
19
+ };
20
+ }
21
+ return { allowed: true, severity: 'info' };
22
+ }
23
+
24
+ export function redactSensitiveValue(value: string): string {
25
+ let redacted = value
26
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email]')
27
+ .replace(/\b(?:\+?\d[\d -]{8,}\d)\b/g, '[phone-or-id]')
28
+ .replace(/\b(?:\d[ -]*?){13,19}\b/g, '[card-like-number]')
29
+ .replace(/\b[A-Za-z0-9_-]{24,}\b/g, '[token-like-value]');
30
+ if (SECRET_LIKE.test(redacted)) redacted = redacted.replace(/:\s*[^,\s}]+/g, ': [redacted]');
31
+ return redacted;
32
+ }
33
+
34
+ export function redactJson(value: unknown): unknown {
35
+ if (typeof value === 'string') return redactSensitiveValue(value);
36
+ if (Array.isArray(value)) return value.map(redactJson);
37
+ if (value && typeof value === 'object') {
38
+ const out: Record<string, unknown> = {};
39
+ for (const [key, item] of Object.entries(value)) {
40
+ if (SECRET_LIKE.test(key)) out[key] = '[redacted]';
41
+ else out[key] = redactJson(item);
42
+ }
43
+ return out;
44
+ }
45
+ return value;
46
+ }
@@ -0,0 +1,167 @@
1
+ export type Industry =
2
+ | 'ecommerce'
3
+ | 'banking'
4
+ | 'fintech'
5
+ | 'healthcare'
6
+ | 'pharma'
7
+ | 'agriculture'
8
+ | 'legacy-enterprise'
9
+ | 'saas'
10
+ | 'unknown';
11
+
12
+ export interface DomainProfile {
13
+ industry: Industry;
14
+ confidence: number;
15
+ importantScreenPatterns: string[];
16
+ workflowGoals: string[];
17
+ piiRules: string[];
18
+ riskyActions: string[];
19
+ safeProbeRules: string[];
20
+ requiredArtifacts: string[];
21
+ evidence: string[];
22
+ }
23
+
24
+ const COMMON_REQUIRED_ARTIFACTS = [
25
+ 'screenshot',
26
+ 'dom',
27
+ 'markdown',
28
+ 'accessibility',
29
+ 'network',
30
+ 'performance',
31
+ ];
32
+
33
+ const PROFILES: Record<Industry, Omit<DomainProfile, 'confidence' | 'evidence'>> = {
34
+ ecommerce: {
35
+ industry: 'ecommerce',
36
+ importantScreenPatterns: ['catalog', 'product', 'search', 'filter', 'cart', 'checkout', 'payment', 'order', 'returns'],
37
+ workflowGoals: ['discover product catalogue', 'map cart and checkout', 'capture order confirmation and failure states', 'map search/filter/sort'],
38
+ piiRules: ['mask email', 'mask phone', 'mask address', 'mask card-like numbers'],
39
+ riskyActions: ['place order', 'submit payment', 'save card', 'apply real coupon'],
40
+ safeProbeRules: ['do not click final payment/order buttons', 'use invalid test card data only', 'stop before irreversible purchase'],
41
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
42
+ },
43
+ banking: {
44
+ industry: 'banking',
45
+ importantScreenPatterns: ['login', 'mfa', 'dashboard', 'accounts', 'transactions', 'transfer', 'beneficiary', 'statement', 'dispute'],
46
+ workflowGoals: ['map account overview', 'map transactions and statements', 'map transfer setup without submitting', 'map MFA/session timeout states'],
47
+ piiRules: ['mask account numbers', 'mask balances', 'mask names', 'mask addresses', 'mask tax identifiers'],
48
+ riskyActions: ['transfer money', 'add beneficiary', 'submit dispute', 'download sensitive statement'],
49
+ safeProbeRules: ['read-only by default', 'never submit transfer confirmation', 'never alter beneficiary data'],
50
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
51
+ },
52
+ fintech: {
53
+ industry: 'fintech',
54
+ importantScreenPatterns: ['kyc', 'wallet', 'transactions', 'cards', 'payments', 'payouts', 'risk', 'compliance', 'disputes'],
55
+ workflowGoals: ['map KYC onboarding', 'map wallet/payment states', 'map failed payment and dispute flows', 'capture compliance screens'],
56
+ piiRules: ['mask card numbers', 'mask bank accounts', 'mask identity documents', 'mask balances'],
57
+ riskyActions: ['send payment', 'withdraw funds', 'change card status', 'submit KYC with real data'],
58
+ safeProbeRules: ['sandbox data only', 'stop before money movement', 'never upload real identity documents'],
59
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
60
+ },
61
+ healthcare: {
62
+ industry: 'healthcare',
63
+ importantScreenPatterns: ['patient', 'appointment', 'claims', 'provider', 'prescription', 'records', 'lab', 'consent'],
64
+ workflowGoals: ['map patient lookup safely', 'map appointment booking before final submit', 'map claims and prescription screens', 'capture consent and records UX'],
65
+ piiRules: ['mask PHI', 'mask patient names', 'mask MRN', 'mask DOB', 'mask diagnosis', 'mask prescriptions'],
66
+ riskyActions: ['schedule real appointment', 'submit claim', 'change medication', 'message provider with PHI'],
67
+ safeProbeRules: ['use synthetic patients only', 'do not submit clinical changes', 'avoid external notifications'],
68
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
69
+ },
70
+ pharma: {
71
+ industry: 'pharma',
72
+ importantScreenPatterns: ['product', 'dosage', 'safety', 'batch', 'adverse-event', 'regulatory', 'trial', 'label'],
73
+ workflowGoals: ['map product and safety content', 'map adverse event intake before submit', 'capture regulatory and batch workflows'],
74
+ piiRules: ['mask patient reports', 'mask reporter contact', 'mask trial participant identifiers'],
75
+ riskyActions: ['submit adverse event', 'change safety record', 'publish regulatory content'],
76
+ safeProbeRules: ['stop before regulatory or safety submission', 'use synthetic reports only'],
77
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
78
+ },
79
+ agriculture: {
80
+ industry: 'agriculture',
81
+ importantScreenPatterns: ['crop', 'field', 'weather', 'inventory', 'marketplace', 'logistics', 'sensor', 'subsidy'],
82
+ workflowGoals: ['map crop and field planning', 'map inventory/marketplace flows', 'capture logistics and sensor dashboards'],
83
+ piiRules: ['mask farmer identity', 'mask location precision', 'mask subsidy identifiers'],
84
+ riskyActions: ['place market order', 'submit subsidy application', 'change sensor/device control'],
85
+ safeProbeRules: ['avoid irreversible orders', 'avoid device actuation', 'use test farm data'],
86
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
87
+ },
88
+ 'legacy-enterprise': {
89
+ industry: 'legacy-enterprise',
90
+ importantScreenPatterns: ['frameset', 'iframe', 'popup', 'table', 'search', 'admin', 'report', 'export'],
91
+ workflowGoals: ['map menu trees', 'capture iframe and popup states', 'map table filters and reports', 'detect session timeout'],
92
+ piiRules: ['mask table cells that look like PII', 'mask exported identifiers'],
93
+ riskyActions: ['bulk update', 'delete record', 'export production data', 'submit admin change'],
94
+ safeProbeRules: ['read-only table probing', 'never click delete/bulk actions', 'capture popups without committing'],
95
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
96
+ },
97
+ saas: {
98
+ industry: 'saas',
99
+ importantScreenPatterns: ['dashboard', 'settings', 'users', 'billing', 'integrations', 'reports', 'admin', 'workspace'],
100
+ workflowGoals: ['map workspace navigation', 'map settings and integrations', 'map role/user management without changing state', 'capture dashboards and reports'],
101
+ piiRules: ['mask emails', 'mask tokens', 'mask API keys', 'mask billing data'],
102
+ riskyActions: ['delete workspace', 'invite real user', 'rotate key', 'change billing plan'],
103
+ safeProbeRules: ['never submit destructive settings', 'redact secrets immediately', 'use sandbox users'],
104
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
105
+ },
106
+ unknown: {
107
+ industry: 'unknown',
108
+ importantScreenPatterns: ['login', 'dashboard', 'settings', 'search', 'detail', 'report', 'form'],
109
+ workflowGoals: ['map top-level navigation', 'capture forms and validation states', 'discover hidden menus', 'classify domain after first pages'],
110
+ piiRules: ['mask emails', 'mask phone numbers', 'mask long identifiers', 'mask card-like numbers'],
111
+ riskyActions: ['delete', 'submit payment', 'transfer', 'publish', 'send notification'],
112
+ safeProbeRules: ['read-only by default', 'stop before irreversible actions', 'use synthetic data only'],
113
+ requiredArtifacts: COMMON_REQUIRED_ARTIFACTS,
114
+ },
115
+ };
116
+
117
+ const INDUSTRY_HINTS: Array<{ industry: Industry; terms: RegExp[] }> = [
118
+ { industry: 'ecommerce', terms: [/cart/i, /checkout/i, /product/i, /catalog/i, /sku/i, /wishlist/i, /order/i] },
119
+ { industry: 'banking', terms: [/account/i, /statement/i, /beneficiar/i, /transfer/i, /iban/i, /routing/i, /balance/i] },
120
+ { industry: 'fintech', terms: [/wallet/i, /kyc/i, /payout/i, /card/i, /payment/i, /risk/i, /dispute/i] },
121
+ { industry: 'healthcare', terms: [/patient/i, /provider/i, /appointment/i, /claim/i, /prescription/i, /medical/i, /lab result/i] },
122
+ { industry: 'pharma', terms: [/pharma/i, /dosage/i, /adverse/i, /batch/i, /regulatory/i, /clinical trial/i, /label/i] },
123
+ { industry: 'agriculture', terms: [/crop/i, /field/i, /farm/i, /harvest/i, /sensor/i, /weather/i, /subsidy/i] },
124
+ { industry: 'legacy-enterprise', terms: [/frameset/i, /iframe/i, /popup/i, /legacy/i, /report export/i, /mainframe/i] },
125
+ { industry: 'saas', terms: [/workspace/i, /tenant/i, /integration/i, /billing/i, /api key/i, /admin/i, /dashboard/i] },
126
+ ];
127
+
128
+ export function classifyDomainProfile(input: {
129
+ appUrl: string;
130
+ projectName?: string | null;
131
+ appType?: string | null;
132
+ seedText?: string;
133
+ }): DomainProfile {
134
+ const haystack = [input.appUrl, input.projectName ?? '', input.appType ?? '', input.seedText ?? ''].join(' ');
135
+ const scores = new Map<Industry, { score: number; evidence: string[] }>();
136
+ for (const { industry, terms } of INDUSTRY_HINTS) {
137
+ for (const term of terms) {
138
+ if (term.test(haystack)) {
139
+ const prev = scores.get(industry) ?? { score: 0, evidence: [] };
140
+ prev.score += 1;
141
+ prev.evidence.push(term.source);
142
+ scores.set(industry, prev);
143
+ }
144
+ }
145
+ }
146
+ let selected: Industry = 'unknown';
147
+ let best = 0;
148
+ let evidence: string[] = [];
149
+ for (const [industry, result] of scores) {
150
+ if (result.score > best) {
151
+ selected = industry;
152
+ best = result.score;
153
+ evidence = result.evidence;
154
+ }
155
+ }
156
+ const base = PROFILES[selected];
157
+ return {
158
+ ...base,
159
+ confidence: best === 0 ? 0.25 : Math.min(0.95, 0.35 + best * 0.15),
160
+ evidence,
161
+ };
162
+ }
163
+
164
+ export function getProfile(industry: Industry): DomainProfile {
165
+ const base = PROFILES[industry] ?? PROFILES.unknown;
166
+ return { ...base, confidence: 1, evidence: ['explicit'] };
167
+ }