@dotsetlabs/bellwether 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. package/CHANGELOG.md +291 -0
  2. package/LICENSE +21 -0
  3. package/README.md +739 -0
  4. package/dist/auth/credentials.d.ts +64 -0
  5. package/dist/auth/credentials.js +218 -0
  6. package/dist/auth/index.d.ts +6 -0
  7. package/dist/auth/index.js +6 -0
  8. package/dist/auth/keychain.d.ts +64 -0
  9. package/dist/auth/keychain.js +268 -0
  10. package/dist/baseline/ab-testing.d.ts +80 -0
  11. package/dist/baseline/ab-testing.js +236 -0
  12. package/dist/baseline/ai-compatibility-scorer.d.ts +95 -0
  13. package/dist/baseline/ai-compatibility-scorer.js +606 -0
  14. package/dist/baseline/calibration.d.ts +77 -0
  15. package/dist/baseline/calibration.js +136 -0
  16. package/dist/baseline/category-matching.d.ts +85 -0
  17. package/dist/baseline/category-matching.js +289 -0
  18. package/dist/baseline/change-impact-analyzer.d.ts +98 -0
  19. package/dist/baseline/change-impact-analyzer.js +592 -0
  20. package/dist/baseline/comparator.d.ts +64 -0
  21. package/dist/baseline/comparator.js +916 -0
  22. package/dist/baseline/confidence.d.ts +55 -0
  23. package/dist/baseline/confidence.js +122 -0
  24. package/dist/baseline/converter.d.ts +61 -0
  25. package/dist/baseline/converter.js +585 -0
  26. package/dist/baseline/dependency-analyzer.d.ts +89 -0
  27. package/dist/baseline/dependency-analyzer.js +567 -0
  28. package/dist/baseline/deprecation-tracker.d.ts +133 -0
  29. package/dist/baseline/deprecation-tracker.js +322 -0
  30. package/dist/baseline/diff.d.ts +55 -0
  31. package/dist/baseline/diff.js +1584 -0
  32. package/dist/baseline/documentation-scorer.d.ts +205 -0
  33. package/dist/baseline/documentation-scorer.js +466 -0
  34. package/dist/baseline/embeddings.d.ts +118 -0
  35. package/dist/baseline/embeddings.js +251 -0
  36. package/dist/baseline/error-analyzer.d.ts +198 -0
  37. package/dist/baseline/error-analyzer.js +721 -0
  38. package/dist/baseline/evaluation/evaluator.d.ts +42 -0
  39. package/dist/baseline/evaluation/evaluator.js +323 -0
  40. package/dist/baseline/evaluation/expanded-dataset.d.ts +45 -0
  41. package/dist/baseline/evaluation/expanded-dataset.js +1164 -0
  42. package/dist/baseline/evaluation/golden-dataset.d.ts +58 -0
  43. package/dist/baseline/evaluation/golden-dataset.js +717 -0
  44. package/dist/baseline/evaluation/index.d.ts +15 -0
  45. package/dist/baseline/evaluation/index.js +15 -0
  46. package/dist/baseline/evaluation/types.d.ts +186 -0
  47. package/dist/baseline/evaluation/types.js +8 -0
  48. package/dist/baseline/external-dependency-detector.d.ts +181 -0
  49. package/dist/baseline/external-dependency-detector.js +524 -0
  50. package/dist/baseline/golden-output.d.ts +162 -0
  51. package/dist/baseline/golden-output.js +636 -0
  52. package/dist/baseline/health-scorer.d.ts +174 -0
  53. package/dist/baseline/health-scorer.js +451 -0
  54. package/dist/baseline/incremental-checker.d.ts +97 -0
  55. package/dist/baseline/incremental-checker.js +174 -0
  56. package/dist/baseline/index.d.ts +31 -0
  57. package/dist/baseline/index.js +42 -0
  58. package/dist/baseline/migration-generator.d.ts +137 -0
  59. package/dist/baseline/migration-generator.js +554 -0
  60. package/dist/baseline/migrations.d.ts +60 -0
  61. package/dist/baseline/migrations.js +197 -0
  62. package/dist/baseline/performance-tracker.d.ts +214 -0
  63. package/dist/baseline/performance-tracker.js +577 -0
  64. package/dist/baseline/pr-comment-generator.d.ts +117 -0
  65. package/dist/baseline/pr-comment-generator.js +546 -0
  66. package/dist/baseline/response-fingerprint.d.ts +127 -0
  67. package/dist/baseline/response-fingerprint.js +728 -0
  68. package/dist/baseline/response-schema-tracker.d.ts +129 -0
  69. package/dist/baseline/response-schema-tracker.js +420 -0
  70. package/dist/baseline/risk-scorer.d.ts +54 -0
  71. package/dist/baseline/risk-scorer.js +434 -0
  72. package/dist/baseline/saver.d.ts +89 -0
  73. package/dist/baseline/saver.js +554 -0
  74. package/dist/baseline/scenario-generator.d.ts +151 -0
  75. package/dist/baseline/scenario-generator.js +905 -0
  76. package/dist/baseline/schema-compare.d.ts +86 -0
  77. package/dist/baseline/schema-compare.js +557 -0
  78. package/dist/baseline/schema-evolution.d.ts +189 -0
  79. package/dist/baseline/schema-evolution.js +467 -0
  80. package/dist/baseline/semantic.d.ts +203 -0
  81. package/dist/baseline/semantic.js +908 -0
  82. package/dist/baseline/synonyms.d.ts +60 -0
  83. package/dist/baseline/synonyms.js +386 -0
  84. package/dist/baseline/telemetry.d.ts +165 -0
  85. package/dist/baseline/telemetry.js +294 -0
  86. package/dist/baseline/test-pruner.d.ts +120 -0
  87. package/dist/baseline/test-pruner.js +387 -0
  88. package/dist/baseline/types.d.ts +449 -0
  89. package/dist/baseline/types.js +5 -0
  90. package/dist/baseline/version.d.ts +138 -0
  91. package/dist/baseline/version.js +206 -0
  92. package/dist/cache/index.d.ts +5 -0
  93. package/dist/cache/index.js +5 -0
  94. package/dist/cache/response-cache.d.ts +151 -0
  95. package/dist/cache/response-cache.js +287 -0
  96. package/dist/ci/index.d.ts +60 -0
  97. package/dist/ci/index.js +342 -0
  98. package/dist/cli/commands/auth.d.ts +12 -0
  99. package/dist/cli/commands/auth.js +352 -0
  100. package/dist/cli/commands/badge.d.ts +3 -0
  101. package/dist/cli/commands/badge.js +74 -0
  102. package/dist/cli/commands/baseline-accept.d.ts +15 -0
  103. package/dist/cli/commands/baseline-accept.js +178 -0
  104. package/dist/cli/commands/baseline-migrate.d.ts +12 -0
  105. package/dist/cli/commands/baseline-migrate.js +164 -0
  106. package/dist/cli/commands/baseline.d.ts +14 -0
  107. package/dist/cli/commands/baseline.js +449 -0
  108. package/dist/cli/commands/beta.d.ts +10 -0
  109. package/dist/cli/commands/beta.js +231 -0
  110. package/dist/cli/commands/check.d.ts +11 -0
  111. package/dist/cli/commands/check.js +820 -0
  112. package/dist/cli/commands/cloud/badge.d.ts +3 -0
  113. package/dist/cli/commands/cloud/badge.js +74 -0
  114. package/dist/cli/commands/cloud/diff.d.ts +6 -0
  115. package/dist/cli/commands/cloud/diff.js +79 -0
  116. package/dist/cli/commands/cloud/history.d.ts +6 -0
  117. package/dist/cli/commands/cloud/history.js +102 -0
  118. package/dist/cli/commands/cloud/link.d.ts +9 -0
  119. package/dist/cli/commands/cloud/link.js +119 -0
  120. package/dist/cli/commands/cloud/login.d.ts +7 -0
  121. package/dist/cli/commands/cloud/login.js +499 -0
  122. package/dist/cli/commands/cloud/projects.d.ts +6 -0
  123. package/dist/cli/commands/cloud/projects.js +44 -0
  124. package/dist/cli/commands/cloud/shared.d.ts +7 -0
  125. package/dist/cli/commands/cloud/shared.js +42 -0
  126. package/dist/cli/commands/cloud/teams.d.ts +8 -0
  127. package/dist/cli/commands/cloud/teams.js +169 -0
  128. package/dist/cli/commands/cloud/upload.d.ts +8 -0
  129. package/dist/cli/commands/cloud/upload.js +181 -0
  130. package/dist/cli/commands/contract.d.ts +11 -0
  131. package/dist/cli/commands/contract.js +280 -0
  132. package/dist/cli/commands/discover.d.ts +3 -0
  133. package/dist/cli/commands/discover.js +82 -0
  134. package/dist/cli/commands/eval.d.ts +9 -0
  135. package/dist/cli/commands/eval.js +187 -0
  136. package/dist/cli/commands/explore.d.ts +11 -0
  137. package/dist/cli/commands/explore.js +437 -0
  138. package/dist/cli/commands/feedback.d.ts +9 -0
  139. package/dist/cli/commands/feedback.js +174 -0
  140. package/dist/cli/commands/golden.d.ts +12 -0
  141. package/dist/cli/commands/golden.js +407 -0
  142. package/dist/cli/commands/history.d.ts +10 -0
  143. package/dist/cli/commands/history.js +202 -0
  144. package/dist/cli/commands/init.d.ts +9 -0
  145. package/dist/cli/commands/init.js +219 -0
  146. package/dist/cli/commands/interview.d.ts +3 -0
  147. package/dist/cli/commands/interview.js +903 -0
  148. package/dist/cli/commands/link.d.ts +10 -0
  149. package/dist/cli/commands/link.js +169 -0
  150. package/dist/cli/commands/login.d.ts +7 -0
  151. package/dist/cli/commands/login.js +499 -0
  152. package/dist/cli/commands/preset.d.ts +33 -0
  153. package/dist/cli/commands/preset.js +297 -0
  154. package/dist/cli/commands/profile.d.ts +33 -0
  155. package/dist/cli/commands/profile.js +286 -0
  156. package/dist/cli/commands/registry.d.ts +11 -0
  157. package/dist/cli/commands/registry.js +146 -0
  158. package/dist/cli/commands/shared.d.ts +79 -0
  159. package/dist/cli/commands/shared.js +196 -0
  160. package/dist/cli/commands/teams.d.ts +8 -0
  161. package/dist/cli/commands/teams.js +169 -0
  162. package/dist/cli/commands/test.d.ts +9 -0
  163. package/dist/cli/commands/test.js +500 -0
  164. package/dist/cli/commands/upload.d.ts +8 -0
  165. package/dist/cli/commands/upload.js +223 -0
  166. package/dist/cli/commands/validate-config.d.ts +6 -0
  167. package/dist/cli/commands/validate-config.js +35 -0
  168. package/dist/cli/commands/verify.d.ts +11 -0
  169. package/dist/cli/commands/verify.js +283 -0
  170. package/dist/cli/commands/watch.d.ts +12 -0
  171. package/dist/cli/commands/watch.js +253 -0
  172. package/dist/cli/index.d.ts +3 -0
  173. package/dist/cli/index.js +178 -0
  174. package/dist/cli/interactive.d.ts +47 -0
  175. package/dist/cli/interactive.js +216 -0
  176. package/dist/cli/output/terminal-reporter.d.ts +19 -0
  177. package/dist/cli/output/terminal-reporter.js +104 -0
  178. package/dist/cli/output.d.ts +226 -0
  179. package/dist/cli/output.js +438 -0
  180. package/dist/cli/utils/env.d.ts +5 -0
  181. package/dist/cli/utils/env.js +14 -0
  182. package/dist/cli/utils/progress.d.ts +59 -0
  183. package/dist/cli/utils/progress.js +206 -0
  184. package/dist/cli/utils/server-context.d.ts +10 -0
  185. package/dist/cli/utils/server-context.js +36 -0
  186. package/dist/cloud/auth.d.ts +144 -0
  187. package/dist/cloud/auth.js +374 -0
  188. package/dist/cloud/client.d.ts +24 -0
  189. package/dist/cloud/client.js +65 -0
  190. package/dist/cloud/http-client.d.ts +38 -0
  191. package/dist/cloud/http-client.js +215 -0
  192. package/dist/cloud/index.d.ts +23 -0
  193. package/dist/cloud/index.js +25 -0
  194. package/dist/cloud/mock-client.d.ts +107 -0
  195. package/dist/cloud/mock-client.js +545 -0
  196. package/dist/cloud/types.d.ts +515 -0
  197. package/dist/cloud/types.js +15 -0
  198. package/dist/config/defaults.d.ts +160 -0
  199. package/dist/config/defaults.js +169 -0
  200. package/dist/config/loader.d.ts +24 -0
  201. package/dist/config/loader.js +122 -0
  202. package/dist/config/template.d.ts +42 -0
  203. package/dist/config/template.js +647 -0
  204. package/dist/config/validator.d.ts +2112 -0
  205. package/dist/config/validator.js +658 -0
  206. package/dist/constants/cloud.d.ts +107 -0
  207. package/dist/constants/cloud.js +110 -0
  208. package/dist/constants/core.d.ts +521 -0
  209. package/dist/constants/core.js +556 -0
  210. package/dist/constants/testing.d.ts +1283 -0
  211. package/dist/constants/testing.js +1568 -0
  212. package/dist/constants.d.ts +10 -0
  213. package/dist/constants.js +10 -0
  214. package/dist/contract/index.d.ts +6 -0
  215. package/dist/contract/index.js +5 -0
  216. package/dist/contract/validator.d.ts +177 -0
  217. package/dist/contract/validator.js +574 -0
  218. package/dist/cost/index.d.ts +6 -0
  219. package/dist/cost/index.js +5 -0
  220. package/dist/cost/tracker.d.ts +134 -0
  221. package/dist/cost/tracker.js +313 -0
  222. package/dist/discovery/discovery.d.ts +16 -0
  223. package/dist/discovery/discovery.js +173 -0
  224. package/dist/discovery/types.d.ts +51 -0
  225. package/dist/discovery/types.js +2 -0
  226. package/dist/docs/agents.d.ts +3 -0
  227. package/dist/docs/agents.js +995 -0
  228. package/dist/docs/contract.d.ts +51 -0
  229. package/dist/docs/contract.js +1681 -0
  230. package/dist/docs/generator.d.ts +4 -0
  231. package/dist/docs/generator.js +4 -0
  232. package/dist/docs/html-reporter.d.ts +9 -0
  233. package/dist/docs/html-reporter.js +757 -0
  234. package/dist/docs/index.d.ts +10 -0
  235. package/dist/docs/index.js +11 -0
  236. package/dist/docs/junit-reporter.d.ts +18 -0
  237. package/dist/docs/junit-reporter.js +210 -0
  238. package/dist/docs/report.d.ts +14 -0
  239. package/dist/docs/report.js +44 -0
  240. package/dist/docs/sarif-reporter.d.ts +19 -0
  241. package/dist/docs/sarif-reporter.js +335 -0
  242. package/dist/docs/shared.d.ts +35 -0
  243. package/dist/docs/shared.js +162 -0
  244. package/dist/docs/templates.d.ts +12 -0
  245. package/dist/docs/templates.js +76 -0
  246. package/dist/errors/index.d.ts +6 -0
  247. package/dist/errors/index.js +6 -0
  248. package/dist/errors/retry.d.ts +92 -0
  249. package/dist/errors/retry.js +323 -0
  250. package/dist/errors/types.d.ts +321 -0
  251. package/dist/errors/types.js +584 -0
  252. package/dist/index.d.ts +32 -0
  253. package/dist/index.js +32 -0
  254. package/dist/interview/dependency-resolver.d.ts +11 -0
  255. package/dist/interview/dependency-resolver.js +32 -0
  256. package/dist/interview/interviewer.d.ts +232 -0
  257. package/dist/interview/interviewer.js +1939 -0
  258. package/dist/interview/mock-response-generator.d.ts +7 -0
  259. package/dist/interview/mock-response-generator.js +102 -0
  260. package/dist/interview/orchestrator.d.ts +237 -0
  261. package/dist/interview/orchestrator.js +1296 -0
  262. package/dist/interview/rate-limiter.d.ts +15 -0
  263. package/dist/interview/rate-limiter.js +55 -0
  264. package/dist/interview/response-validator.d.ts +10 -0
  265. package/dist/interview/response-validator.js +132 -0
  266. package/dist/interview/schema-inferrer.d.ts +8 -0
  267. package/dist/interview/schema-inferrer.js +71 -0
  268. package/dist/interview/schema-test-generator.d.ts +71 -0
  269. package/dist/interview/schema-test-generator.js +834 -0
  270. package/dist/interview/smart-value-generator.d.ts +155 -0
  271. package/dist/interview/smart-value-generator.js +554 -0
  272. package/dist/interview/stateful-test-runner.d.ts +19 -0
  273. package/dist/interview/stateful-test-runner.js +106 -0
  274. package/dist/interview/types.d.ts +561 -0
  275. package/dist/interview/types.js +2 -0
  276. package/dist/llm/anthropic.d.ts +41 -0
  277. package/dist/llm/anthropic.js +355 -0
  278. package/dist/llm/client.d.ts +123 -0
  279. package/dist/llm/client.js +42 -0
  280. package/dist/llm/factory.d.ts +38 -0
  281. package/dist/llm/factory.js +145 -0
  282. package/dist/llm/fallback.d.ts +140 -0
  283. package/dist/llm/fallback.js +379 -0
  284. package/dist/llm/index.d.ts +18 -0
  285. package/dist/llm/index.js +15 -0
  286. package/dist/llm/ollama.d.ts +37 -0
  287. package/dist/llm/ollama.js +330 -0
  288. package/dist/llm/openai.d.ts +25 -0
  289. package/dist/llm/openai.js +320 -0
  290. package/dist/llm/token-budget.d.ts +161 -0
  291. package/dist/llm/token-budget.js +395 -0
  292. package/dist/logging/logger.d.ts +70 -0
  293. package/dist/logging/logger.js +130 -0
  294. package/dist/metrics/collector.d.ts +106 -0
  295. package/dist/metrics/collector.js +547 -0
  296. package/dist/metrics/index.d.ts +7 -0
  297. package/dist/metrics/index.js +7 -0
  298. package/dist/metrics/prometheus.d.ts +20 -0
  299. package/dist/metrics/prometheus.js +241 -0
  300. package/dist/metrics/types.d.ts +209 -0
  301. package/dist/metrics/types.js +5 -0
  302. package/dist/persona/builtins.d.ts +54 -0
  303. package/dist/persona/builtins.js +219 -0
  304. package/dist/persona/index.d.ts +8 -0
  305. package/dist/persona/index.js +8 -0
  306. package/dist/persona/loader.d.ts +30 -0
  307. package/dist/persona/loader.js +190 -0
  308. package/dist/persona/types.d.ts +144 -0
  309. package/dist/persona/types.js +5 -0
  310. package/dist/persona/validation.d.ts +94 -0
  311. package/dist/persona/validation.js +332 -0
  312. package/dist/prompts/index.d.ts +5 -0
  313. package/dist/prompts/index.js +5 -0
  314. package/dist/prompts/templates.d.ts +180 -0
  315. package/dist/prompts/templates.js +431 -0
  316. package/dist/registry/client.d.ts +49 -0
  317. package/dist/registry/client.js +191 -0
  318. package/dist/registry/index.d.ts +7 -0
  319. package/dist/registry/index.js +6 -0
  320. package/dist/registry/types.d.ts +140 -0
  321. package/dist/registry/types.js +6 -0
  322. package/dist/scenarios/evaluator.d.ts +43 -0
  323. package/dist/scenarios/evaluator.js +206 -0
  324. package/dist/scenarios/index.d.ts +10 -0
  325. package/dist/scenarios/index.js +9 -0
  326. package/dist/scenarios/loader.d.ts +20 -0
  327. package/dist/scenarios/loader.js +285 -0
  328. package/dist/scenarios/types.d.ts +153 -0
  329. package/dist/scenarios/types.js +8 -0
  330. package/dist/security/index.d.ts +17 -0
  331. package/dist/security/index.js +18 -0
  332. package/dist/security/payloads.d.ts +61 -0
  333. package/dist/security/payloads.js +268 -0
  334. package/dist/security/security-tester.d.ts +42 -0
  335. package/dist/security/security-tester.js +582 -0
  336. package/dist/security/types.d.ts +166 -0
  337. package/dist/security/types.js +8 -0
  338. package/dist/transport/base-transport.d.ts +59 -0
  339. package/dist/transport/base-transport.js +38 -0
  340. package/dist/transport/http-transport.d.ts +67 -0
  341. package/dist/transport/http-transport.js +238 -0
  342. package/dist/transport/mcp-client.d.ts +141 -0
  343. package/dist/transport/mcp-client.js +496 -0
  344. package/dist/transport/sse-transport.d.ts +88 -0
  345. package/dist/transport/sse-transport.js +316 -0
  346. package/dist/transport/stdio-transport.d.ts +43 -0
  347. package/dist/transport/stdio-transport.js +238 -0
  348. package/dist/transport/types.d.ts +125 -0
  349. package/dist/transport/types.js +16 -0
  350. package/dist/utils/concurrency.d.ts +123 -0
  351. package/dist/utils/concurrency.js +213 -0
  352. package/dist/utils/formatters.d.ts +16 -0
  353. package/dist/utils/formatters.js +37 -0
  354. package/dist/utils/index.d.ts +8 -0
  355. package/dist/utils/index.js +8 -0
  356. package/dist/utils/jsonpath.d.ts +87 -0
  357. package/dist/utils/jsonpath.js +326 -0
  358. package/dist/utils/markdown.d.ts +113 -0
  359. package/dist/utils/markdown.js +265 -0
  360. package/dist/utils/network.d.ts +14 -0
  361. package/dist/utils/network.js +17 -0
  362. package/dist/utils/sanitize.d.ts +92 -0
  363. package/dist/utils/sanitize.js +191 -0
  364. package/dist/utils/semantic.d.ts +194 -0
  365. package/dist/utils/semantic.js +1051 -0
  366. package/dist/utils/smart-truncate.d.ts +94 -0
  367. package/dist/utils/smart-truncate.js +361 -0
  368. package/dist/utils/timeout.d.ts +153 -0
  369. package/dist/utils/timeout.js +205 -0
  370. package/dist/utils/yaml-parser.d.ts +58 -0
  371. package/dist/utils/yaml-parser.js +86 -0
  372. package/dist/validation/index.d.ts +32 -0
  373. package/dist/validation/index.js +32 -0
  374. package/dist/validation/semantic-test-generator.d.ts +50 -0
  375. package/dist/validation/semantic-test-generator.js +176 -0
  376. package/dist/validation/semantic-types.d.ts +66 -0
  377. package/dist/validation/semantic-types.js +94 -0
  378. package/dist/validation/semantic-validator.d.ts +38 -0
  379. package/dist/validation/semantic-validator.js +340 -0
  380. package/dist/verification/index.d.ts +6 -0
  381. package/dist/verification/index.js +5 -0
  382. package/dist/verification/types.d.ts +133 -0
  383. package/dist/verification/types.js +5 -0
  384. package/dist/verification/verifier.d.ts +30 -0
  385. package/dist/verification/verifier.js +309 -0
  386. package/dist/version.d.ts +19 -0
  387. package/dist/version.js +48 -0
  388. package/dist/workflow/auto-generator.d.ts +27 -0
  389. package/dist/workflow/auto-generator.js +513 -0
  390. package/dist/workflow/discovery.d.ts +40 -0
  391. package/dist/workflow/discovery.js +195 -0
  392. package/dist/workflow/executor.d.ts +82 -0
  393. package/dist/workflow/executor.js +611 -0
  394. package/dist/workflow/index.d.ts +10 -0
  395. package/dist/workflow/index.js +10 -0
  396. package/dist/workflow/loader.d.ts +24 -0
  397. package/dist/workflow/loader.js +194 -0
  398. package/dist/workflow/state-tracker.d.ts +98 -0
  399. package/dist/workflow/state-tracker.js +424 -0
  400. package/dist/workflow/types.d.ts +337 -0
  401. package/dist/workflow/types.js +5 -0
  402. package/package.json +94 -0
  403. package/schemas/bellwether-check.schema.json +651 -0
@@ -0,0 +1,1051 @@
1
+ /**
2
+ * Semantic text analysis utilities.
3
+ *
4
+ * Provides stemming, negation handling, constraint normalization,
5
+ * and enhanced keyword extraction for semantic matching.
6
+ */
7
+ /**
8
+ * Common English suffixes for stemming.
9
+ * This is a simplified Porter-like stemmer that handles common cases.
10
+ */
11
+ const SUFFIX_RULES = [
12
+ // Plurals
13
+ { suffix: 'ies', replacement: 'y', minLength: 4 },
14
+ { suffix: 'es', replacement: '', minLength: 4 },
15
+ { suffix: 's', replacement: '', minLength: 4 },
16
+ // Past tense and gerunds
17
+ { suffix: 'ied', replacement: 'y', minLength: 4 },
18
+ { suffix: 'ed', replacement: '', minLength: 4 },
19
+ { suffix: 'ing', replacement: '', minLength: 5 },
20
+ // Adverbs and adjectives
21
+ { suffix: 'ly', replacement: '', minLength: 4 },
22
+ { suffix: 'ness', replacement: '', minLength: 5 },
23
+ { suffix: 'ment', replacement: '', minLength: 5 },
24
+ { suffix: 'able', replacement: '', minLength: 5 },
25
+ { suffix: 'ible', replacement: '', minLength: 5 },
26
+ { suffix: 'tion', replacement: '', minLength: 5 },
27
+ { suffix: 'sion', replacement: '', minLength: 5 },
28
+ { suffix: 'ity', replacement: '', minLength: 4 },
29
+ { suffix: 'ful', replacement: '', minLength: 4 },
30
+ { suffix: 'less', replacement: '', minLength: 5 },
31
+ { suffix: 'ive', replacement: '', minLength: 4 },
32
+ { suffix: 'ous', replacement: '', minLength: 4 },
33
+ { suffix: 'er', replacement: '', minLength: 4 },
34
+ { suffix: 'est', replacement: '', minLength: 4 },
35
+ ];
36
+ /**
37
+ * Irregular word mappings that don't follow suffix rules.
38
+ */
39
+ const IRREGULAR_STEMS = {
40
+ // Verbs
41
+ ran: 'run',
42
+ running: 'run',
43
+ runs: 'run',
44
+ wrote: 'write',
45
+ written: 'write',
46
+ writes: 'write',
47
+ writing: 'write',
48
+ read: 'read',
49
+ reads: 'read',
50
+ reading: 'read',
51
+ went: 'go',
52
+ goes: 'go',
53
+ going: 'go',
54
+ gone: 'go',
55
+ was: 'be',
56
+ were: 'be',
57
+ been: 'be',
58
+ being: 'be',
59
+ had: 'have',
60
+ has: 'have',
61
+ having: 'have',
62
+ did: 'do',
63
+ does: 'do',
64
+ doing: 'do',
65
+ made: 'make',
66
+ makes: 'make',
67
+ making: 'make',
68
+ took: 'take',
69
+ takes: 'take',
70
+ taking: 'take',
71
+ taken: 'take',
72
+ got: 'get',
73
+ gets: 'get',
74
+ getting: 'get',
75
+ threw: 'throw',
76
+ throws: 'throw',
77
+ throwing: 'throw',
78
+ thrown: 'throw',
79
+ found: 'find',
80
+ finds: 'find',
81
+ finding: 'find',
82
+ caught: 'catch',
83
+ catches: 'catch',
84
+ catching: 'catch',
85
+ sent: 'send',
86
+ sends: 'send',
87
+ sending: 'send',
88
+ built: 'build',
89
+ builds: 'build',
90
+ building: 'build',
91
+ // Nouns
92
+ files: 'file',
93
+ directories: 'directory',
94
+ paths: 'path',
95
+ errors: 'error',
96
+ exceptions: 'exception',
97
+ requests: 'request',
98
+ responses: 'response',
99
+ users: 'user',
100
+ attacks: 'attack',
101
+ vulnerabilities: 'vulnerability',
102
+ injections: 'injection',
103
+ children: 'child',
104
+ data: 'datum',
105
+ // Technical terms
106
+ authenticated: 'auth',
107
+ authentication: 'auth',
108
+ authenticates: 'auth',
109
+ authorized: 'author',
110
+ authorization: 'author',
111
+ authorizes: 'author',
112
+ validated: 'valid',
113
+ validates: 'valid',
114
+ validation: 'valid',
115
+ sanitized: 'sanit',
116
+ sanitizes: 'sanit',
117
+ sanitization: 'sanit',
118
+ encrypted: 'encrypt',
119
+ encrypts: 'encrypt',
120
+ encryption: 'encrypt',
121
+ decrypted: 'decrypt',
122
+ decrypts: 'decrypt',
123
+ decryption: 'decrypt',
124
+ };
125
+ /**
126
+ * Stem a single word using simplified Porter-like rules.
127
+ *
128
+ * @param word - Word to stem (should be lowercase)
129
+ * @returns Stemmed word
130
+ */
131
+ export function stem(word) {
132
+ if (!word || word.length < 3)
133
+ return word;
134
+ // Check irregular mappings first
135
+ if (IRREGULAR_STEMS[word]) {
136
+ return IRREGULAR_STEMS[word];
137
+ }
138
+ // Apply suffix rules
139
+ for (const rule of SUFFIX_RULES) {
140
+ if (word.length >= rule.minLength && word.endsWith(rule.suffix)) {
141
+ const stemmed = word.slice(0, -rule.suffix.length) + rule.replacement;
142
+ // Don't stem if result is too short
143
+ if (stemmed.length >= 2) {
144
+ return stemmed;
145
+ }
146
+ }
147
+ }
148
+ return word;
149
+ }
150
+ /**
151
+ * Stem all words in a text.
152
+ *
153
+ * @param text - Text to stem
154
+ * @returns Text with all words stemmed
155
+ */
156
+ export function stemText(text) {
157
+ return text
158
+ .toLowerCase()
159
+ .split(/\s+/)
160
+ .map(word => stem(word.replace(/[^a-z0-9]/g, '')))
161
+ .filter(w => w.length > 0)
162
+ .join(' ');
163
+ }
164
+ /**
165
+ * Extract keywords with stemming applied.
166
+ *
167
+ * @param text - Text to extract keywords from
168
+ * @returns Set of stemmed keywords
169
+ */
170
+ export function extractStemmedKeywords(text) {
171
+ const stopWords = new Set([
172
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
173
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
174
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
175
+ 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
176
+ 'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after',
177
+ 'above', 'below', 'between', 'under', 'again', 'further', 'then',
178
+ 'once', 'and', 'but', 'or', 'nor', 'so', 'yet', 'both', 'either',
179
+ 'neither', 'not', 'only', 'own', 'same', 'than', 'too', 'very',
180
+ 'just', 'also', 'now', 'here', 'there', 'when', 'where', 'why',
181
+ 'how', 'all', 'each', 'every', 'any', 'some', 'no', 'such', 'what',
182
+ 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'it', 'its',
183
+ ]);
184
+ const words = text
185
+ .toLowerCase()
186
+ .replace(/[^a-z0-9\s]/g, ' ')
187
+ .split(/\s+/)
188
+ .filter(w => w.length > 2 && !stopWords.has(w))
189
+ .map(w => stem(w));
190
+ return new Set(words);
191
+ }
192
+ /**
193
+ * Calculate keyword overlap with stemming.
194
+ *
195
+ * @param text1 - First text
196
+ * @param text2 - Second text
197
+ * @returns Overlap percentage (0-100)
198
+ */
199
+ export function calculateStemmedKeywordOverlap(text1, text2) {
200
+ const words1 = extractStemmedKeywords(text1);
201
+ const words2 = extractStemmedKeywords(text2);
202
+ if (words1.size === 0 && words2.size === 0)
203
+ return 100;
204
+ if (words1.size === 0 || words2.size === 0)
205
+ return 0;
206
+ const intersection = new Set([...words1].filter(w => words2.has(w)));
207
+ const union = new Set([...words1, ...words2]);
208
+ return Math.round((intersection.size / union.size) * 100);
209
+ }
210
+ /**
211
+ * Negation patterns that flip the meaning of following words.
212
+ */
213
+ const NEGATION_PATTERNS = [
214
+ /\bnot\s+(\w+)/gi,
215
+ /\bno\s+(\w+)/gi,
216
+ /\bnever\s+(\w+)/gi,
217
+ /\bwithout\s+(\w+)/gi,
218
+ /\bdoes\s*n[o']t\s+(\w+)/gi,
219
+ /\bisn[o']t\s+(\w+)/gi,
220
+ /\baren[o']t\s+(\w+)/gi,
221
+ /\bwasn[o']t\s+(\w+)/gi,
222
+ /\bweren[o']t\s+(\w+)/gi,
223
+ /\bcan[o']t\s+(\w+)/gi,
224
+ /\bcannot\s+(\w+)/gi,
225
+ /\bwon[o']t\s+(\w+)/gi,
226
+ /\bdon[o']t\s+(\w+)/gi,
227
+ /\bdoesn[o']t\s+(\w+)/gi,
228
+ /\bshouldn[o']t\s+(\w+)/gi,
229
+ /\bwouldn[o']t\s+(\w+)/gi,
230
+ /\bcouldn[o']t\s+(\w+)/gi,
231
+ /\bunlike(ly)?\s+(\w+)/gi,
232
+ /\bimpossible\b/gi,
233
+ /\bnon[-_]?(\w+)/gi,
234
+ ];
235
+ /**
236
+ * Keywords that indicate severity levels.
237
+ */
238
+ const SEVERITY_KEYWORDS = {
239
+ critical: ['critical', 'severe', 'rce', 'remote code execution', 'arbitrary code', 'complete compromise'],
240
+ high: ['high', 'dangerous', 'injection', 'traversal', 'lfi', 'ssrf', 'arbitrary file', 'xxe', 'deserialization', 'unsafe', '../', '..\\'],
241
+ medium: ['medium', 'moderate', 'potential', 'possible', 'may lead', 'could allow'],
242
+ low: ['low', 'minor', 'informational', 'best practice', 'recommendation'],
243
+ };
244
+ /**
245
+ * Analyze text for negation patterns.
246
+ *
247
+ * @param text - Text to analyze
248
+ * @returns Negation analysis result
249
+ */
250
+ export function analyzeNegation(text) {
251
+ const negatedWords = [];
252
+ let markedText = text;
253
+ for (const pattern of NEGATION_PATTERNS) {
254
+ let match;
255
+ // Reset lastIndex for global patterns
256
+ pattern.lastIndex = 0;
257
+ while ((match = pattern.exec(text)) !== null) {
258
+ // The negated word is typically in the first or second capture group
259
+ const negatedWord = match[1] || match[2];
260
+ if (negatedWord) {
261
+ negatedWords.push(negatedWord.toLowerCase());
262
+ markedText = markedText.replace(match[0], `[NEGATED:${match[0]}]`);
263
+ }
264
+ }
265
+ }
266
+ return {
267
+ negatedWords,
268
+ isNegated: negatedWords.length > 0,
269
+ markedText,
270
+ };
271
+ }
272
+ /**
273
+ * Check if a severity keyword is negated in the text.
274
+ *
275
+ * @param text - Text to check
276
+ * @param keyword - Severity keyword to look for
277
+ * @returns True if keyword is negated
278
+ */
279
+ export function isSeverityNegated(text, keyword) {
280
+ const lowerText = text.toLowerCase();
281
+ const keywordIndex = lowerText.indexOf(keyword.toLowerCase());
282
+ if (keywordIndex === -1)
283
+ return false;
284
+ // Check if there's a negation within 3 words before the keyword
285
+ const beforeText = lowerText.slice(Math.max(0, keywordIndex - 30), keywordIndex);
286
+ const negationIndicators = [
287
+ 'not ', 'no ', 'never ', 'without ', "isn't ", "aren't ", "wasn't ",
288
+ "weren't ", "don't ", "doesn't ", "didn't ", "won't ", "can't ",
289
+ "cannot ", "shouldn't ", "wouldn't ", "couldn't ", 'non-', 'non_',
290
+ 'unlikely', 'not considered', 'not a ',
291
+ ];
292
+ return negationIndicators.some(neg => beforeText.includes(neg));
293
+ }
294
+ /**
295
+ * Extract severity from text with negation handling.
296
+ *
297
+ * @param text - Text to extract severity from
298
+ * @returns Extracted severity level
299
+ */
300
+ export function extractSeverityWithNegation(text) {
301
+ const lowerText = text.toLowerCase();
302
+ // Check each severity level from highest to lowest
303
+ for (const [level, keywords] of Object.entries(SEVERITY_KEYWORDS)) {
304
+ for (const keyword of keywords) {
305
+ if (lowerText.includes(keyword)) {
306
+ // Check if this keyword is negated
307
+ if (isSeverityNegated(text, keyword)) {
308
+ // If negated, skip to next keyword
309
+ continue;
310
+ }
311
+ return level;
312
+ }
313
+ }
314
+ }
315
+ // Default to low if no keywords found
316
+ return 'low';
317
+ }
318
+ /**
319
+ * Size unit multipliers to bytes.
320
+ */
321
+ const SIZE_UNITS = {
322
+ b: 1,
323
+ byte: 1,
324
+ bytes: 1,
325
+ kb: 1024,
326
+ kilobyte: 1024,
327
+ kilobytes: 1024,
328
+ kib: 1024,
329
+ mb: 1024 * 1024,
330
+ megabyte: 1024 * 1024,
331
+ megabytes: 1024 * 1024,
332
+ mib: 1024 * 1024,
333
+ gb: 1024 * 1024 * 1024,
334
+ gigabyte: 1024 * 1024 * 1024,
335
+ gigabytes: 1024 * 1024 * 1024,
336
+ gib: 1024 * 1024 * 1024,
337
+ tb: 1024 * 1024 * 1024 * 1024,
338
+ terabyte: 1024 * 1024 * 1024 * 1024,
339
+ terabytes: 1024 * 1024 * 1024 * 1024,
340
+ tib: 1024 * 1024 * 1024 * 1024,
341
+ };
342
+ /**
343
+ * Time unit multipliers to milliseconds.
344
+ */
345
+ const TIME_UNITS = {
346
+ ms: 1,
347
+ millisecond: 1,
348
+ milliseconds: 1,
349
+ s: 1000,
350
+ sec: 1000,
351
+ secs: 1000,
352
+ second: 1000,
353
+ seconds: 1000,
354
+ m: 60 * 1000,
355
+ min: 60 * 1000,
356
+ mins: 60 * 1000,
357
+ minute: 60 * 1000,
358
+ minutes: 60 * 1000,
359
+ h: 60 * 60 * 1000,
360
+ hr: 60 * 60 * 1000,
361
+ hrs: 60 * 60 * 1000,
362
+ hour: 60 * 60 * 1000,
363
+ hours: 60 * 60 * 1000,
364
+ d: 24 * 60 * 60 * 1000,
365
+ day: 24 * 60 * 60 * 1000,
366
+ days: 24 * 60 * 60 * 1000,
367
+ };
368
+ /**
369
+ * Rate unit multipliers to per-second.
370
+ */
371
+ const RATE_UNITS = {
372
+ '/s': 1,
373
+ '/sec': 1,
374
+ '/second': 1,
375
+ 'per second': 1,
376
+ 'per sec': 1,
377
+ '/m': 1 / 60,
378
+ '/min': 1 / 60,
379
+ '/minute': 1 / 60,
380
+ 'per minute': 1 / 60,
381
+ 'per min': 1 / 60,
382
+ '/h': 1 / 3600,
383
+ '/hr': 1 / 3600,
384
+ '/hour': 1 / 3600,
385
+ 'per hour': 1 / 3600,
386
+ 'per hr': 1 / 3600,
387
+ '/d': 1 / 86400,
388
+ '/day': 1 / 86400,
389
+ 'per day': 1 / 86400,
390
+ };
391
+ /**
392
+ * Parse and normalize a constraint value.
393
+ *
394
+ * @param constraint - Constraint string (e.g., "10MB", "30 seconds", "100 requests/min")
395
+ * @returns Normalized constraint or undefined if not parseable
396
+ */
397
+ export function normalizeConstraint(constraint) {
398
+ if (!constraint)
399
+ return undefined;
400
+ const original = constraint.trim();
401
+ const lower = original.toLowerCase();
402
+ // Try to match size pattern: number followed by size unit
403
+ const sizeMatch = lower.match(/^(\d+(?:\.\d+)?)\s*([a-z]+)$/);
404
+ if (sizeMatch) {
405
+ const value = parseFloat(sizeMatch[1]);
406
+ const unit = sizeMatch[2];
407
+ if (SIZE_UNITS[unit] !== undefined) {
408
+ return {
409
+ original,
410
+ type: 'size',
411
+ value,
412
+ unit,
413
+ baseValue: value * SIZE_UNITS[unit],
414
+ };
415
+ }
416
+ if (TIME_UNITS[unit] !== undefined) {
417
+ return {
418
+ original,
419
+ type: 'time',
420
+ value,
421
+ unit,
422
+ baseValue: value * TIME_UNITS[unit],
423
+ };
424
+ }
425
+ }
426
+ // Try to match rate pattern: number [unit] per/slash time
427
+ const rateMatch = lower.match(/^(\d+(?:\.\d+)?)\s*(?:requests?|calls?|ops?|operations?|queries?)?\s*(per\s+\w+|\/\w+)$/);
428
+ if (rateMatch) {
429
+ const value = parseFloat(rateMatch[1]);
430
+ const rateUnit = rateMatch[2];
431
+ if (RATE_UNITS[rateUnit] !== undefined) {
432
+ return {
433
+ original,
434
+ type: 'rate',
435
+ value,
436
+ unit: rateUnit,
437
+ baseValue: value * RATE_UNITS[rateUnit],
438
+ };
439
+ }
440
+ }
441
+ // Try plain number (count)
442
+ const countMatch = lower.match(/^(\d+(?:\.\d+)?)$/);
443
+ if (countMatch) {
444
+ const value = parseFloat(countMatch[1]);
445
+ return {
446
+ original,
447
+ type: 'count',
448
+ value,
449
+ unit: '',
450
+ baseValue: value,
451
+ };
452
+ }
453
+ return undefined;
454
+ }
455
+ /**
456
+ * Format types that are considered equivalent or related.
457
+ */
458
+ const FORMAT_EQUIVALENTS = {
459
+ json: ['json'],
460
+ xml: ['xml'],
461
+ csv: ['csv'],
462
+ yaml: ['yaml', 'yml'],
463
+ html: ['html', 'htm'],
464
+ text: ['text', 'txt', 'plain'],
465
+ binary: ['binary', 'bin'],
466
+ };
467
+ /**
468
+ * Compare two constraint values with unit normalization.
469
+ *
470
+ * @param a - First constraint
471
+ * @param b - Second constraint
472
+ * @returns Similarity score (0-100)
473
+ */
474
+ export function compareConstraints(a, b) {
475
+ if (!a && !b)
476
+ return 100;
477
+ if (!a || !b)
478
+ return 50;
479
+ const normA = normalizeConstraint(a);
480
+ const normB = normalizeConstraint(b);
481
+ // Check if both are format types (non-numeric strings)
482
+ const cleanA = a.replace(/\s/g, '').toLowerCase();
483
+ const cleanB = b.replace(/\s/g, '').toLowerCase();
484
+ // Check if these are format type strings (json, xml, etc.)
485
+ const isFormatA = Object.keys(FORMAT_EQUIVALENTS).some(fmt => FORMAT_EQUIVALENTS[fmt].includes(cleanA));
486
+ const isFormatB = Object.keys(FORMAT_EQUIVALENTS).some(fmt => FORMAT_EQUIVALENTS[fmt].includes(cleanB));
487
+ if (isFormatA && isFormatB) {
488
+ // Both are format types - compare them
489
+ const formatA = Object.keys(FORMAT_EQUIVALENTS).find(fmt => FORMAT_EQUIVALENTS[fmt].includes(cleanA));
490
+ const formatB = Object.keys(FORMAT_EQUIVALENTS).find(fmt => FORMAT_EQUIVALENTS[fmt].includes(cleanB));
491
+ return formatA === formatB ? 100 : 0; // Format types must match exactly
492
+ }
493
+ // If both couldn't be parsed, do string comparison
494
+ if (!normA && !normB) {
495
+ return cleanA === cleanB ? 100 : 30;
496
+ }
497
+ // One parsed, one didn't
498
+ if (!normA || !normB)
499
+ return 40;
500
+ // Different types of constraints
501
+ if (normA.type !== normB.type)
502
+ return 20;
503
+ // Same type - compare base values
504
+ if (normA.baseValue === normB.baseValue)
505
+ return 100;
506
+ // Close values (within 10%)
507
+ const ratio = Math.min(normA.baseValue, normB.baseValue) / Math.max(normA.baseValue, normB.baseValue);
508
+ if (ratio > 0.9)
509
+ return 90;
510
+ if (ratio > 0.8)
511
+ return 75;
512
+ if (ratio > 0.5)
513
+ return 50;
514
+ return 30;
515
+ }
516
+ /**
517
+ * Extended security category keywords including new categories.
518
+ */
519
+ export const EXTENDED_SECURITY_KEYWORDS = {
520
+ path_traversal: [
521
+ 'path traversal', 'directory traversal', '../', '..\\', 'lfi',
522
+ 'local file inclusion', 'arbitrary file', 'file path manipulation',
523
+ 'escape directory', 'outside base', 'outside allowed', 'read files',
524
+ 'directory escape', 'file access', 'traverse', 'dot dot slash',
525
+ ],
526
+ command_injection: [
527
+ 'command injection', 'shell injection', 'os command', 'exec',
528
+ 'system(', 'subprocess', 'shell=true', 'code execution',
529
+ 'system call', 'execute command', 'command execution', 'shell command',
530
+ ],
531
+ sql_injection: [
532
+ 'sql injection', 'sqli', 'query injection', 'database injection',
533
+ 'union select', 'drop table', 'or 1=1', 'inject sql', 'sql can be injected',
534
+ 'malicious sql', 'sql vulnerability', 'unsanitized sql', 'sql command',
535
+ 'database query', 'sql statement', 'parameterized', 'prepared statement',
536
+ ],
537
+ xss: [
538
+ 'xss', 'cross-site scripting', 'script injection', 'html injection',
539
+ 'dom-based', 'reflected xss', 'stored xss', 'cross site', 'javascript injection',
540
+ 'without encoding', 'unescaped output', 'unsanitized output', 'xss vulnerability',
541
+ ],
542
+ xxe: [
543
+ 'xxe', 'xml external entity', 'xml injection', 'entity expansion',
544
+ 'billion laughs', 'dtd injection', 'xml bomb',
545
+ ],
546
+ ssrf: [
547
+ 'ssrf', 'server-side request forgery', 'internal network',
548
+ 'localhost access', 'cloud metadata', '169.254.169.254',
549
+ 'internal services', 'server side request',
550
+ 'internal resources', 'access internal',
551
+ ],
552
+ deserialization: [
553
+ 'deserialization', 'unsafe deserialization', 'object injection',
554
+ 'pickle', 'yaml.load', 'unserialize', 'readobject',
555
+ ],
556
+ timing_attack: [
557
+ 'timing attack', 'side-channel', 'timing side channel',
558
+ 'constant-time', 'timing oracle', 'cache timing',
559
+ ],
560
+ race_condition: [
561
+ 'race condition', 'toctou', 'time of check', 'concurrency bug',
562
+ 'check-then-use', 'double-checked locking',
563
+ ],
564
+ file_upload: [
565
+ 'file upload', 'arbitrary upload', 'unrestricted upload',
566
+ 'malicious file', 'upload bypass', 'content-type bypass',
567
+ ],
568
+ access_control: [
569
+ 'access control', 'unauthorized access', 'privilege escalation',
570
+ 'bypass', 'idor', 'insecure direct object',
571
+ ],
572
+ authentication: [
573
+ 'authentication', 'auth bypass', 'credential', 'password',
574
+ 'login', 'brute force', 'credential stuffing',
575
+ ],
576
+ authorization: [
577
+ 'authorization', 'permission', 'role', 'access denied',
578
+ 'forbidden', 'rbac bypass', 'acl bypass',
579
+ ],
580
+ information_disclosure: [
581
+ 'information disclosure', 'data leak', 'sensitive data',
582
+ 'expose', 'reveals', 'verbose error', 'stack trace',
583
+ ],
584
+ denial_of_service: [
585
+ 'denial of service', 'dos', 'resource exhaustion', 'infinite loop',
586
+ 'crash', 'regex dos', 'redos', 'algorithmic complexity',
587
+ ],
588
+ input_validation: [
589
+ 'input validation', 'sanitization', 'validation', 'untrusted input',
590
+ 'user input', 'malformed input', 'validate input', 'input sanitization',
591
+ ],
592
+ output_encoding: [
593
+ 'output encoding', 'escape', 'encoding', 'sanitize output',
594
+ 'context-aware encoding',
595
+ ],
596
+ cryptography: [
597
+ 'cryptography', 'encryption', 'hashing', 'random', 'weak cipher',
598
+ 'hardcoded key', 'insecure random', 'md5', 'sha1', 'ecb mode',
599
+ ],
600
+ session_management: [
601
+ 'session', 'cookie', 'token', 'jwt', 'session fixation',
602
+ 'session hijacking', 'insecure cookie',
603
+ ],
604
+ error_handling: [
605
+ 'error handling', 'exception', 'stack trace', 'verbose error',
606
+ 'error message', 'unhandled exception',
607
+ ],
608
+ logging: [
609
+ 'logging', 'audit', 'sensitive log', 'log injection',
610
+ 'insufficient logging',
611
+ ],
612
+ configuration: [
613
+ 'configuration', 'hardcoded', 'default', 'insecure default',
614
+ 'misconfiguration', 'debug mode',
615
+ ],
616
+ prototype_pollution: [
617
+ 'prototype pollution', '__proto__', 'constructor.prototype',
618
+ 'object pollution',
619
+ ],
620
+ open_redirect: [
621
+ 'open redirect', 'url redirect', 'redirect vulnerability',
622
+ 'unvalidated redirect',
623
+ ],
624
+ clickjacking: [
625
+ 'clickjacking', 'ui redress', 'frame injection', 'x-frame-options',
626
+ ],
627
+ cors: [
628
+ 'cors', 'cross-origin', 'access-control-allow-origin',
629
+ 'cors misconfiguration',
630
+ ],
631
+ csp_bypass: [
632
+ 'csp bypass', 'content security policy', 'script-src bypass',
633
+ ],
634
+ other: [],
635
+ };
636
+ /**
637
+ * Extract security category from text using extended keywords.
638
+ *
639
+ * @param text - Text to analyze
640
+ * @returns Detected security category
641
+ */
642
+ export function extractSecurityCategoryExtended(text) {
643
+ const lowerText = text.toLowerCase();
644
+ for (const [category, keywords] of Object.entries(EXTENDED_SECURITY_KEYWORDS)) {
645
+ if (keywords.some(keyword => lowerText.includes(keyword))) {
646
+ return category;
647
+ }
648
+ }
649
+ return 'other';
650
+ }
651
+ /**
652
+ * Check if two texts are semantically similar considering stemming.
653
+ *
654
+ * @param text1 - First text
655
+ * @param text2 - Second text
656
+ * @param threshold - Minimum similarity threshold (0-100, default 60)
657
+ * @returns True if texts are similar
658
+ */
659
+ export function areSemanticallySimular(text1, text2, threshold = 60) {
660
+ return calculateStemmedKeywordOverlap(text1, text2) >= threshold;
661
+ }
662
+ /**
663
+ * Extract database type qualifier from text.
664
+ * Distinguishes SQL from NoSQL/MongoDB/Redis etc.
665
+ */
666
+ export function extractDatabaseQualifier(text) {
667
+ const lower = text.toLowerCase();
668
+ // Check for explicit NoSQL indicators BEFORE SQL check
669
+ // (otherwise "NoSQL" would match "SQL" first)
670
+ if (lower.includes('nosql') ||
671
+ lower.includes('no-sql') ||
672
+ lower.includes('non-sql') ||
673
+ lower.includes('document database') ||
674
+ lower.includes('key-value')) {
675
+ return 'nosql';
676
+ }
677
+ // Specific database types
678
+ if (lower.includes('mongodb') || lower.includes('mongo db')) {
679
+ return 'mongodb';
680
+ }
681
+ if (lower.includes('redis')) {
682
+ return 'redis';
683
+ }
684
+ // Generic SQL (checked after NoSQL to avoid false matches)
685
+ if (lower.includes('sql') && !lower.includes('nosql')) {
686
+ return 'sql';
687
+ }
688
+ return 'generic';
689
+ }
690
+ /**
691
+ * Extract direction qualifier from text.
692
+ * Distinguishes upload from download, read from write.
693
+ */
694
+ export function extractDirectionQualifier(text) {
695
+ const lower = text.toLowerCase();
696
+ // Upload indicators
697
+ if (lower.includes('upload') ||
698
+ lower.includes('incoming') ||
699
+ lower.includes('receive') ||
700
+ lower.includes('inbound') ||
701
+ lower.includes('sent to server')) {
702
+ return 'upload';
703
+ }
704
+ // Download indicators
705
+ if (lower.includes('download') ||
706
+ lower.includes('outgoing') ||
707
+ lower.includes('fetch') ||
708
+ lower.includes('outbound') ||
709
+ lower.includes('retrieve') ||
710
+ lower.includes('from server')) {
711
+ return 'download';
712
+ }
713
+ // Read vs write
714
+ if (lower.includes('read') && !lower.includes('write')) {
715
+ return 'read';
716
+ }
717
+ if (lower.includes('write') && !lower.includes('read')) {
718
+ return 'write';
719
+ }
720
+ return 'generic';
721
+ }
722
+ /**
723
+ * Extract timeout type qualifier from text.
724
+ * Distinguishes connection timeout from read/write/request timeouts.
725
+ */
726
+ export function extractTimeoutQualifier(text) {
727
+ const lower = text.toLowerCase();
728
+ if (lower.includes('connection timeout') ||
729
+ lower.includes('connect timeout') ||
730
+ lower.includes('connection time')) {
731
+ return 'connection';
732
+ }
733
+ if (lower.includes('read timeout') ||
734
+ lower.includes('reading timeout') ||
735
+ lower.includes('socket read')) {
736
+ return 'read';
737
+ }
738
+ if (lower.includes('write timeout') ||
739
+ lower.includes('writing timeout') ||
740
+ lower.includes('socket write')) {
741
+ return 'write';
742
+ }
743
+ if (lower.includes('request timeout')) {
744
+ return 'request';
745
+ }
746
+ if (lower.includes('response timeout')) {
747
+ return 'response';
748
+ }
749
+ if (lower.includes('idle timeout') ||
750
+ lower.includes('inactivity timeout')) {
751
+ return 'idle';
752
+ }
753
+ return 'generic';
754
+ }
755
+ /**
756
+ * Extract rate limit time unit from text.
757
+ * Distinguishes per-second from per-minute from per-hour limits.
758
+ */
759
+ export function extractRateTimeUnit(text) {
760
+ const lower = text.toLowerCase();
761
+ // Per second patterns
762
+ if (lower.includes('per second') ||
763
+ lower.includes('/s') ||
764
+ lower.includes('/sec') ||
765
+ lower.includes('per sec')) {
766
+ return 'second';
767
+ }
768
+ // Per minute patterns
769
+ if (lower.includes('per minute') ||
770
+ lower.includes('/m') ||
771
+ lower.includes('/min') ||
772
+ lower.includes('per min') ||
773
+ lower.includes('rpm')) {
774
+ return 'minute';
775
+ }
776
+ // Per hour patterns
777
+ if (lower.includes('per hour') ||
778
+ lower.includes('/h') ||
779
+ lower.includes('/hr') ||
780
+ lower.includes('per hr') ||
781
+ lower.includes('hourly')) {
782
+ return 'hour';
783
+ }
784
+ // Per day patterns
785
+ if (lower.includes('per day') ||
786
+ lower.includes('/d') ||
787
+ lower.includes('daily') ||
788
+ lower.includes('per 24')) {
789
+ return 'day';
790
+ }
791
+ return 'unknown';
792
+ }
793
+ /**
794
+ * Detect overall polarity of an assertion.
795
+ * Returns 'negative' if the statement is negated/denied.
796
+ */
797
+ export function detectPolarity(text) {
798
+ const lower = text.toLowerCase();
799
+ // Strong negative indicators at the start
800
+ const negativeStarters = [
801
+ 'not ', 'no ', 'never ', 'without ', 'lacks ', 'missing ',
802
+ 'does not ', "doesn't ", 'cannot ', "can't ", 'will not ',
803
+ "won't ", 'should not ', "shouldn't ", 'must not ', "mustn't ",
804
+ 'is not ', "isn't ", 'are not ', "aren't ", 'was not ', "wasn't ",
805
+ 'were not ', "weren't ", 'has not ', "hasn't ", 'have not ', "haven't ",
806
+ 'did not ', "didn't ", 'do not ', "don't ", 'does not ', "doesn't ",
807
+ 'unable to ', 'fails to ', 'failed to ', 'prevents ', 'blocks ',
808
+ 'denies ', 'rejects ', 'refuses ', 'prohibits ', 'disallows ',
809
+ ];
810
+ // Check if text starts with negative
811
+ for (const starter of negativeStarters) {
812
+ if (lower.startsWith(starter)) {
813
+ return 'negative';
814
+ }
815
+ }
816
+ // Check for "not a/an" patterns indicating absence
817
+ if (/not\s+a\s+\w+/.test(lower) ||
818
+ /not\s+an\s+\w+/.test(lower) ||
819
+ /no\s+\w+\s+(vulnerability|issue|problem|risk|threat)/.test(lower) ||
820
+ /is\s+not\s+\w+/.test(lower)) {
821
+ return 'negative';
822
+ }
823
+ // Positive affirmation patterns
824
+ const positiveIndicators = [
825
+ 'is a ', 'is an ', 'contains ', 'includes ', 'has ', 'found ',
826
+ 'detected ', 'identified ', 'discovered ', 'confirmed ', 'exists ',
827
+ 'present ', 'vulnerable to ', 'affected by ', 'susceptible to ',
828
+ ];
829
+ for (const indicator of positiveIndicators) {
830
+ if (lower.includes(indicator)) {
831
+ return 'positive';
832
+ }
833
+ }
834
+ return 'neutral';
835
+ }
836
+ /**
837
+ * Check if a security finding or assertion is negated.
838
+ * Returns true if the text explicitly denies the assertion/vulnerability.
839
+ */
840
+ export function isSecurityFindingNegated(text) {
841
+ const lower = text.toLowerCase();
842
+ // Patterns that explicitly deny a vulnerability or assertion
843
+ const negationPatterns = [
844
+ // Vulnerability negations
845
+ /not\s+(a\s+)?(critical|high|medium|low|severe)\s+vulnerab/i,
846
+ /no\s+(critical|high|medium|low)\s+(severity\s+)?vulnerab/i,
847
+ /not\s+vulnerable\s+to/i,
848
+ /no\s+vulnerab/i,
849
+ /vulnerab\w*\s+(was\s+)?not\s+found/i,
850
+ /no\s+(security\s+)?(issues?|problems?|risks?|threats?)\s+found/i,
851
+ /does\s+not\s+(have|contain|exhibit)\s+\w*\s*vulnerab/i,
852
+ /lacks?\s+\w*\s*vulnerab/i,
853
+ /absence\s+of\s+\w*\s*vulnerab/i,
854
+ /free\s+(from|of)\s+\w*\s*vulnerab/i,
855
+ /passed\s+security/i,
856
+ /security\s+check\s+passed/i,
857
+ /is\s+secure/i,
858
+ /not\s+affected/i,
859
+ /not\s+susceptible/i,
860
+ // General action negations (for assertions)
861
+ /is\s+not\s+(validated|required|enabled|allowed|supported)/i,
862
+ /\b(not|never)\s+(validated|required|enabled|allowed|supported|checked|verified)\b/i,
863
+ /\b(disabled|disallowed|unsupported|unchecked|unverified)\b/i,
864
+ /\bno\s+(size|rate|time)\s+limit\b/i,
865
+ /\b(lacks?|missing|without)\s+(validation|authentication|authorization)/i,
866
+ ];
867
+ for (const pattern of negationPatterns) {
868
+ if (pattern.test(lower)) {
869
+ return true;
870
+ }
871
+ }
872
+ return false;
873
+ }
874
+ /**
875
+ * Extract all qualifiers from text.
876
+ * Provides comprehensive context for semantic comparison.
877
+ */
878
+ export function extractQualifiers(text) {
879
+ return {
880
+ database: extractDatabaseQualifier(text),
881
+ direction: extractDirectionQualifier(text),
882
+ timeout: extractTimeoutQualifier(text),
883
+ polarity: detectPolarity(text),
884
+ isNegated: isSecurityFindingNegated(text),
885
+ rateTimeUnit: extractRateTimeUnit(text),
886
+ };
887
+ }
888
+ /**
889
+ * Opposite term pairs that indicate incompatible meanings.
890
+ * When one text contains one term and the other contains its opposite,
891
+ * they should not match.
892
+ *
893
+ * Format: [term1, term2, useWordBoundary]
894
+ * useWordBoundary: true if we should match as whole words (prevents "asynchronous" matching "synchronous")
895
+ */
896
+ const OPPOSITE_TERMS = [
897
+ // State opposites (need word boundaries to avoid substring matches)
898
+ ['enabled', 'disabled', false],
899
+ ['required', 'optional', false],
900
+ ['synchronous', 'asynchronous', true], // word boundary to avoid substring match
901
+ ['sync', 'async', true], // abbreviations
902
+ ['horizontal', 'vertical', true],
903
+ ['read', 'write', true], // word boundary for "read" vs "write"
904
+ ['upload', 'download', false],
905
+ ['input', 'output', false],
906
+ ['success', 'failure', false],
907
+ ['valid', 'invalid', false],
908
+ ['secure', 'insecure', false],
909
+ ['encrypted', 'unencrypted', false],
910
+ ['authenticated', 'unauthenticated', false],
911
+ ['authorized', 'unauthorized', false],
912
+ // Quantity opposites
913
+ ['limited', 'unlimited', false],
914
+ // HTTP status code opposites
915
+ ['200', '201', true], // Different success codes
916
+ ['200', '404', true],
917
+ ['200', '500', true],
918
+ // Severity opposites
919
+ ['high', 'low', true],
920
+ ['critical', 'low', true],
921
+ // Security type opposites
922
+ ['server-side', 'cross-site', true],
923
+ ['ssrf', 'csrf', true],
924
+ ['xss', 'csrf', true],
925
+ ['local file', 'remote file', false],
926
+ ['lfi', 'rfi', true],
927
+ // v1.3.0: Additional behavior opposites for better assertion matching
928
+ ['error', 'null', true], // Different return types
929
+ ['null', 'default', true], // Different return values
930
+ ['throws', 'returns', true], // Different error handling
931
+ ['creates', 'fails', true], // Different file behaviors
932
+ ['creates', 'deletes', true],
933
+ ['exists', 'not found', false],
934
+ ['found', 'missing', true],
935
+ // Format opposites
936
+ ['json', 'text', true],
937
+ ['json', 'plain text', false],
938
+ ['binary', 'text', true],
939
+ // Rate limit time units
940
+ ['per minute', 'per hour', false],
941
+ ['per second', 'per minute', false],
942
+ ['per second', 'per hour', false],
943
+ // Limit presence opposites
944
+ ['no limit', 'limit of', false],
945
+ ['no size limit', 'size limit', false],
946
+ ];
947
+ /**
948
+ * Check if a word exists in text as a whole word (not as substring).
949
+ */
950
+ function containsWord(text, word) {
951
+ const regex = new RegExp(`\\b${word}\\b`, 'i');
952
+ return regex.test(text);
953
+ }
954
+ /**
955
+ * Check if two texts contain opposite terms.
956
+ */
957
+ function containsOppositeTerms(text1, text2) {
958
+ const lower1 = text1.toLowerCase();
959
+ const lower2 = text2.toLowerCase();
960
+ for (const [term1, term2, useWordBoundary] of OPPOSITE_TERMS) {
961
+ let has1InText1;
962
+ let has2InText2;
963
+ let has1InText2;
964
+ let has2InText1;
965
+ if (useWordBoundary) {
966
+ has1InText1 = containsWord(lower1, term1);
967
+ has2InText2 = containsWord(lower2, term2);
968
+ has1InText2 = containsWord(lower1, term2);
969
+ has2InText1 = containsWord(lower2, term1);
970
+ }
971
+ else {
972
+ has1InText1 = lower1.includes(term1);
973
+ has2InText2 = lower2.includes(term2);
974
+ has1InText2 = lower1.includes(term2);
975
+ has2InText1 = lower2.includes(term1);
976
+ }
977
+ // Check if text1 has term1 and text2 has term2 (but not vice versa)
978
+ if (has1InText1 && has2InText2 && !has1InText2 && !has2InText1) {
979
+ return `${term1} vs ${term2}`;
980
+ }
981
+ // Check if text1 has term2 and text2 has term1 (but not vice versa)
982
+ if (has2InText1 && has1InText2 && !has1InText1 && !has2InText2) {
983
+ return `${term2} vs ${term1}`;
984
+ }
985
+ }
986
+ return null;
987
+ }
988
+ /**
989
+ * Compare qualifiers between two texts.
990
+ * Returns a compatibility score (0-100).
991
+ */
992
+ export function compareQualifiers(text1, text2) {
993
+ const q1 = extractQualifiers(text1);
994
+ const q2 = extractQualifiers(text2);
995
+ const incompatibilities = [];
996
+ let score = 100;
997
+ // Negation mismatch is fatal - positive and negative can't match
998
+ if ((q1.isNegated && !q2.isNegated) || (!q1.isNegated && q2.isNegated)) {
999
+ incompatibilities.push('negation mismatch (one affirms, one denies)');
1000
+ score -= 80; // Almost always a mismatch
1001
+ }
1002
+ // Polarity mismatch (weaker than negation)
1003
+ if (q1.polarity !== q2.polarity && q1.polarity !== 'neutral' && q2.polarity !== 'neutral') {
1004
+ if ((q1.polarity === 'positive' && q2.polarity === 'negative') ||
1005
+ (q1.polarity === 'negative' && q2.polarity === 'positive')) {
1006
+ incompatibilities.push(`polarity mismatch (${q1.polarity} vs ${q2.polarity})`);
1007
+ score -= 40;
1008
+ }
1009
+ }
1010
+ // Check for opposite terms (enabled vs disabled, synchronous vs asynchronous, etc.)
1011
+ const oppositeTerms = containsOppositeTerms(text1, text2);
1012
+ if (oppositeTerms) {
1013
+ incompatibilities.push(`opposite terms: ${oppositeTerms}`);
1014
+ score -= 60;
1015
+ }
1016
+ // Database qualifier mismatch (SQL vs NoSQL is incompatible)
1017
+ if (q1.database !== 'generic' && q2.database !== 'generic' && q1.database !== q2.database) {
1018
+ incompatibilities.push(`database type mismatch (${q1.database} vs ${q2.database})`);
1019
+ score -= 60; // Increased penalty
1020
+ }
1021
+ // Direction qualifier mismatch (upload vs download is incompatible)
1022
+ if (q1.direction !== 'generic' && q2.direction !== 'generic' && q1.direction !== q2.direction) {
1023
+ incompatibilities.push(`direction mismatch (${q1.direction} vs ${q2.direction})`);
1024
+ score -= 50; // Increased penalty
1025
+ }
1026
+ // Timeout type mismatch
1027
+ if (q1.timeout !== 'generic' && q2.timeout !== 'generic' && q1.timeout !== q2.timeout) {
1028
+ incompatibilities.push(`timeout type mismatch (${q1.timeout} vs ${q2.timeout})`);
1029
+ score -= 55; // Increased penalty to ensure score < 50
1030
+ }
1031
+ // Rate time unit mismatch (per minute vs per hour is different)
1032
+ if (q1.rateTimeUnit !== 'unknown' && q2.rateTimeUnit !== 'unknown' &&
1033
+ q1.rateTimeUnit !== q2.rateTimeUnit) {
1034
+ incompatibilities.push(`rate time unit mismatch (${q1.rateTimeUnit} vs ${q2.rateTimeUnit})`);
1035
+ score -= 55; // Increased penalty to ensure score < 50
1036
+ }
1037
+ return {
1038
+ score: Math.max(0, score),
1039
+ incompatibilities,
1040
+ };
1041
+ }
1042
+ /**
1043
+ * Check if two texts have compatible qualifiers for matching.
1044
+ * Returns false if there are critical incompatibilities.
1045
+ */
1046
+ export function qualifiersCompatible(text1, text2) {
1047
+ const { score } = compareQualifiers(text1, text2);
1048
+ // Require more than 50% compatibility for texts to match (stricter threshold)
1049
+ return score > 50;
1050
+ }
1051
+ //# sourceMappingURL=semantic.js.map