@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,908 @@
1
+ /**
2
+ * Semantic comparison utilities for drift detection.
3
+ *
4
+ * This module provides robust comparison that handles LLM non-determinism
5
+ * by normalizing text and extracting structured facts rather than comparing
6
+ * raw prose strings.
7
+ */
8
+ import { calculateKeywordOverlap, calculateLengthSimilarity, calculateSemanticIndicators, CONFIDENCE_WEIGHTS, } from './confidence.js';
9
+ import { extractSeverityWithNegation, compareConstraints, EXTENDED_SECURITY_KEYWORDS, calculateStemmedKeywordOverlap, compareQualifiers, isSecurityFindingNegated, } from '../utils/semantic.js';
10
+ import { extractSecurityCategories, extractLimitationCategories, findBestSecurityCategoryMatch, findBestLimitationCategoryMatch, calculateSecurityCategoryRelationship, calculateLimitationCategoryRelationship, } from './category-matching.js';
11
+ import { findSharedSecurityTerms, calculateSynonymSimilarity, expandAbbreviations, timeExpressionsEqual, } from './synonyms.js';
12
+ /**
13
+ * Security finding categories (normalized).
14
+ * These map to common vulnerability patterns.
15
+ * Extended to include additional security categories like XXE, timing attacks, etc.
16
+ */
17
+ export const SECURITY_CATEGORIES = [
18
+ 'path_traversal',
19
+ 'command_injection',
20
+ 'sql_injection',
21
+ 'xss',
22
+ 'xxe',
23
+ 'ssrf',
24
+ 'deserialization',
25
+ 'timing_attack',
26
+ 'race_condition',
27
+ 'file_upload',
28
+ 'access_control',
29
+ 'authentication',
30
+ 'authorization',
31
+ 'information_disclosure',
32
+ 'denial_of_service',
33
+ 'input_validation',
34
+ 'output_encoding',
35
+ 'cryptography',
36
+ 'session_management',
37
+ 'error_handling',
38
+ 'logging',
39
+ 'configuration',
40
+ 'prototype_pollution',
41
+ 'open_redirect',
42
+ 'clickjacking',
43
+ 'cors',
44
+ 'csp_bypass',
45
+ 'other',
46
+ ];
47
+ /**
48
+ * Limitation categories (normalized).
49
+ */
50
+ export const LIMITATION_CATEGORIES = [
51
+ 'size_limit',
52
+ 'rate_limit',
53
+ 'timeout',
54
+ 'encoding',
55
+ 'format',
56
+ 'permission',
57
+ 'platform',
58
+ 'dependency',
59
+ 'concurrency',
60
+ 'memory',
61
+ 'network',
62
+ 'other',
63
+ ];
64
+ /**
65
+ * Keywords that map to security categories.
66
+ * Now using EXTENDED_SECURITY_KEYWORDS from the semantic utilities module
67
+ * which includes additional categories like XXE, timing attacks, etc.
68
+ */
69
+ const SECURITY_KEYWORDS = EXTENDED_SECURITY_KEYWORDS;
70
+ /**
71
+ * Keywords that map to limitation categories.
72
+ */
73
+ const LIMITATION_KEYWORDS = {
74
+ size_limit: ['size limit', 'max size', 'file size', 'mb', 'gb', 'kb', 'bytes', 'too large', 'megabytes', 'gigabytes', 'kilobytes'],
75
+ rate_limit: ['rate limit', 'throttle', 'requests per', 'quota', 'too many requests'],
76
+ timeout: ['timeout', 'time out', 'time limit', 'seconds', 'ms', 'timed out', 'deadline'],
77
+ encoding: ['encoding', 'utf-8', 'ascii', 'binary', 'charset', 'unicode'],
78
+ format: ['format', 'json', 'xml', 'csv', 'type', 'mime', 'content-type'],
79
+ permission: ['permission', 'access', 'denied', 'forbidden', 'read-only', 'write'],
80
+ platform: ['platform', 'windows', 'linux', 'macos', 'os-specific'],
81
+ dependency: ['dependency', 'requires', 'prerequisite', 'library', 'package'],
82
+ concurrency: ['concurrent', 'parallel', 'thread', 'lock', 'race condition'],
83
+ memory: ['memory', 'ram', 'heap', 'out of memory'],
84
+ network: ['network', 'connection', 'offline', 'unreachable'],
85
+ other: [],
86
+ };
87
+ /**
88
+ * Extract security category from text.
89
+ */
90
+ export function extractSecurityCategory(text) {
91
+ const lowerText = text.toLowerCase();
92
+ for (const [category, keywords] of Object.entries(SECURITY_KEYWORDS)) {
93
+ if (keywords.some(keyword => lowerText.includes(keyword))) {
94
+ return category;
95
+ }
96
+ }
97
+ return 'other';
98
+ }
99
+ /**
100
+ * Extract limitation category from text.
101
+ */
102
+ export function extractLimitationCategory(text) {
103
+ const lowerText = text.toLowerCase();
104
+ for (const [category, keywords] of Object.entries(LIMITATION_KEYWORDS)) {
105
+ if (keywords.some(keyword => lowerText.includes(keyword))) {
106
+ return category;
107
+ }
108
+ }
109
+ return 'other';
110
+ }
111
+ /**
112
+ * Extract severity from text.
113
+ * Now uses negation-aware extraction to handle phrases like "not critical".
114
+ */
115
+ export function extractSeverity(text) {
116
+ // Use the enhanced negation-aware severity extraction
117
+ return extractSeverityWithNegation(text);
118
+ }
119
+ /**
120
+ * Create a normalized fingerprint from assertion text.
121
+ * This extracts key semantic elements for comparison.
122
+ *
123
+ * For assertions about limitations or security, we primarily use
124
+ * the category to ensure semantic equivalence (e.g., "10MB limit" and
125
+ * "files larger than 10 megabytes" both get category 'size_limit').
126
+ */
127
+ export function createFingerprint(tool, aspect, text) {
128
+ const lowerText = text.toLowerCase();
129
+ // Extract key elements
130
+ const elements = [tool, aspect];
131
+ // For error_handling assertions (often derived from limitations),
132
+ // include the limitation category for semantic grouping
133
+ if (aspect === 'error_handling') {
134
+ const category = extractLimitationCategory(text);
135
+ if (category !== 'other') {
136
+ elements.push(`limit:${category}`);
137
+ }
138
+ }
139
+ // For security aspects, include the security category
140
+ if (aspect === 'security') {
141
+ const category = extractSecurityCategory(text);
142
+ if (category !== 'other') {
143
+ elements.push(`sec:${category}`);
144
+ }
145
+ }
146
+ // Extract action verbs
147
+ const actions = ['returns', 'throws', 'fails', 'succeeds', 'handles', 'validates', 'rejects', 'accepts', 'creates', 'deletes', 'reads', 'writes'];
148
+ for (const action of actions) {
149
+ if (lowerText.includes(action)) {
150
+ elements.push(action);
151
+ }
152
+ }
153
+ // Extract condition keywords (but skip if we already have a category)
154
+ const hasCategory = elements.some(e => e.startsWith('limit:') || e.startsWith('sec:'));
155
+ if (!hasCategory) {
156
+ const conditions = ['error', 'invalid', 'missing', 'empty', 'null', 'undefined', 'exists', 'not found', 'permission', 'timeout'];
157
+ for (const condition of conditions) {
158
+ if (lowerText.includes(condition)) {
159
+ elements.push(condition.replace(' ', '_'));
160
+ }
161
+ }
162
+ }
163
+ // Extract output keywords
164
+ const outputs = ['success', 'failure', 'true', 'false', 'json', 'string', 'array', 'object', 'number', 'boolean'];
165
+ for (const output of outputs) {
166
+ if (lowerText.includes(output)) {
167
+ elements.push(output);
168
+ }
169
+ }
170
+ // Sort for consistency and join
171
+ return elements.sort().join(':');
172
+ }
173
+ /**
174
+ * Convert raw security notes to structured findings.
175
+ */
176
+ export function structureSecurityNotes(tool, notes) {
177
+ return notes.map(note => ({
178
+ category: extractSecurityCategory(note),
179
+ tool,
180
+ severity: extractSeverity(note),
181
+ description: note,
182
+ }));
183
+ }
184
+ /**
185
+ * Convert raw limitations to structured limitations.
186
+ */
187
+ export function structureLimitations(tool, limitations) {
188
+ return limitations.map(limitation => ({
189
+ category: extractLimitationCategory(limitation),
190
+ tool,
191
+ constraint: extractConstraint(limitation),
192
+ description: limitation,
193
+ }));
194
+ }
195
+ /**
196
+ * Extract constraint from text (e.g., "10MB", "100 requests", "JSON").
197
+ * Handles numeric constraints and format/type names.
198
+ */
199
+ function extractConstraint(text) {
200
+ // Match patterns like "10MB", "100 requests/min", "30 seconds"
201
+ const numericPatterns = [
202
+ /(\d+\s*[kmgt]?b)/i, // Size: 10MB, 1GB
203
+ /(\d+\s*(?:ms|seconds?|minutes?|hours?))/i, // Time
204
+ /(\d+\s*(?:requests?|calls?)(?:\s*\/\s*\w+)?)/i, // Rate
205
+ ];
206
+ for (const pattern of numericPatterns) {
207
+ const match = text.match(pattern);
208
+ if (match) {
209
+ return match[1].trim();
210
+ }
211
+ }
212
+ // Extract format types (JSON, XML, CSV, etc.)
213
+ const formatPattern = /\b(json|xml|csv|yaml|toml|html|text|binary|utf-?8|ascii)\b/i;
214
+ const formatMatch = text.match(formatPattern);
215
+ if (formatMatch) {
216
+ return formatMatch[1].toLowerCase();
217
+ }
218
+ return undefined;
219
+ }
220
+ /**
221
+ * Compare two structured security findings.
222
+ * Returns true if they represent the same finding.
223
+ */
224
+ export function securityFindingsMatch(a, b) {
225
+ return (a.category === b.category &&
226
+ a.tool === b.tool &&
227
+ a.severity === b.severity);
228
+ }
229
+ /**
230
+ * Compare two structured security findings with confidence.
231
+ * Returns a confidence score indicating how similar they are.
232
+ *
233
+ * ENHANCED (v1.1.0): Uses multi-category detection and relationship scoring
234
+ * to improve recall. Categories that are related (e.g., authentication and
235
+ * authorization) now get partial credit instead of 0%.
236
+ *
237
+ * ENHANCED (v1.2.0): Added qualifier comparison to prevent false positives from:
238
+ * - Negation mismatches ("Critical vulnerability found" vs "Not a critical vulnerability")
239
+ * - Database type mismatches (SQL injection vs NoSQL injection)
240
+ *
241
+ * ENHANCED (v1.3.0): Improved recall by:
242
+ * - Adding synonym-based similarity detection
243
+ * - Relaxing severity mismatch (no longer blocks matching)
244
+ * - Lowering thresholds when shared security terms are found
245
+ * - Better handling of abbreviations (SQLi, XSS, SSRF)
246
+ */
247
+ export function securityFindingsMatchWithConfidence(a, b) {
248
+ const factors = [];
249
+ // Expand abbreviations for better matching
250
+ const descA = expandAbbreviations(a.description);
251
+ const descB = expandAbbreviations(b.description);
252
+ // CRITICAL: Check for negation mismatch FIRST
253
+ // A finding that affirms a vulnerability cannot match one that denies it
254
+ const aNegated = isSecurityFindingNegated(a.description);
255
+ const bNegated = isSecurityFindingNegated(b.description);
256
+ const negationMismatch = aNegated !== bNegated;
257
+ if (negationMismatch) {
258
+ factors.push({
259
+ name: 'negation_check',
260
+ weight: 0.2,
261
+ value: 0,
262
+ description: `Negation mismatch: ${aNegated ? 'first denies' : 'first affirms'}, ${bNegated ? 'second denies' : 'second affirms'}`,
263
+ });
264
+ }
265
+ // Check qualifier compatibility (SQL vs NoSQL, etc.)
266
+ // Only block if there's a clear incompatibility, not just different wording
267
+ const qualifierComparison = compareQualifiers(a.description, b.description);
268
+ const hasHardIncompatibility = qualifierComparison.incompatibilities.some(inc => inc.includes('sql vs nosql') || inc.includes('ssrf vs csrf'));
269
+ if (qualifierComparison.incompatibilities.length > 0) {
270
+ factors.push({
271
+ name: 'qualifier_compatibility',
272
+ weight: 0.1,
273
+ value: qualifierComparison.score,
274
+ description: `Qualifier issues: ${qualifierComparison.incompatibilities.join(', ')}`,
275
+ });
276
+ }
277
+ // Check for shared security terms (synonym-based matching)
278
+ const sharedTerms = findSharedSecurityTerms(descA, descB);
279
+ const hasSharedSecurityTerms = sharedTerms.length > 0;
280
+ if (hasSharedSecurityTerms) {
281
+ factors.push({
282
+ name: 'shared_security_terms',
283
+ weight: 0.25,
284
+ value: Math.min(100, sharedTerms.length * 50),
285
+ description: `Shared security concepts: ${sharedTerms.join(', ')}`,
286
+ });
287
+ }
288
+ // Synonym-based similarity (catches paraphrases with different wording)
289
+ const synonymSimilarity = calculateSynonymSimilarity(descA, descB, 'security');
290
+ if (synonymSimilarity > 0) {
291
+ factors.push({
292
+ name: 'synonym_similarity',
293
+ weight: 0.15,
294
+ value: synonymSimilarity,
295
+ description: `${synonymSimilarity}% synonym-based similarity`,
296
+ });
297
+ }
298
+ // Multi-category detection: extract ALL categories from descriptions
299
+ const categoriesA = extractSecurityCategories(descA);
300
+ const categoriesB = extractSecurityCategories(descB);
301
+ // Find best matching category pair using relationship scoring
302
+ const bestCategoryMatch = findBestSecurityCategoryMatch(categoriesA, categoriesB);
303
+ // Calculate category relationship score (allows partial credit for related categories)
304
+ let categoryScore;
305
+ let categoryDescription;
306
+ if (a.category === b.category) {
307
+ // Exact category match from structured data
308
+ categoryScore = 100;
309
+ categoryDescription = `Categories match exactly: ${a.category}`;
310
+ }
311
+ else if (bestCategoryMatch && bestCategoryMatch.relationshipScore >= 60) {
312
+ // Related categories found via multi-category detection (lowered threshold)
313
+ categoryScore = bestCategoryMatch.relationshipScore;
314
+ categoryDescription = `Related categories: ${bestCategoryMatch.cat1} ~ ${bestCategoryMatch.cat2} (${bestCategoryMatch.relationshipScore}% related)`;
315
+ }
316
+ else if (hasSharedSecurityTerms) {
317
+ // Shared security terms suggest the same vulnerability type
318
+ categoryScore = 80;
319
+ categoryDescription = `Categories inferred from shared terms: ${sharedTerms.join(', ')}`;
320
+ }
321
+ else {
322
+ // Check direct relationship between structured categories
323
+ const directRelationship = calculateSecurityCategoryRelationship(a.category, b.category);
324
+ categoryScore = Math.max(directRelationship, 20); // Minimum 20 for any security finding
325
+ categoryDescription = directRelationship >= 50
326
+ ? `Related categories: ${a.category} ~ ${b.category} (${directRelationship}% related)`
327
+ : `Categories differ: ${a.category} vs ${b.category}`;
328
+ }
329
+ // Category match (25% weight - reduced to make room for synonyms)
330
+ factors.push({
331
+ name: 'category_match',
332
+ weight: 0.25,
333
+ value: categoryScore,
334
+ description: categoryDescription,
335
+ });
336
+ // Tool match (10% weight)
337
+ const toolMatch = a.tool === b.tool;
338
+ factors.push({
339
+ name: 'tool_match',
340
+ weight: 0.1,
341
+ value: toolMatch ? 100 : 50,
342
+ description: toolMatch
343
+ ? `Tools match: ${a.tool}`
344
+ : `Different tools: ${a.tool} vs ${b.tool}`,
345
+ });
346
+ // Severity match (10% weight - reduced, severity differences are often benign)
347
+ // IMPORTANT: Severity mismatch no longer blocks matching, only affects confidence
348
+ const severityMatch = a.severity === b.severity;
349
+ const severityScore = severityMatch ? 100 : severityDistance(a.severity, b.severity);
350
+ factors.push({
351
+ name: 'severity_match',
352
+ weight: 0.1,
353
+ value: severityScore,
354
+ description: severityMatch
355
+ ? `Severities match: ${a.severity}`
356
+ : `Severities differ: ${a.severity} vs ${b.severity} (${severityScore}% similar)`,
357
+ });
358
+ // Description similarity with synonym expansion
359
+ const descSimilarity = calculateStemmedKeywordOverlap(descA, descB);
360
+ factors.push({
361
+ name: 'description_similarity',
362
+ weight: 0.15,
363
+ value: descSimilarity,
364
+ description: `${descSimilarity}% description keyword overlap (stemmed)`,
365
+ });
366
+ const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
367
+ const score = Math.round(factors.reduce((sum, f) => sum + f.weight * f.value, 0) / totalWeight);
368
+ // CRITICAL: Only block matching for clear semantic conflicts:
369
+ // 1. Negation mismatch (one affirms, one denies)
370
+ // 2. Hard qualifier incompatibility (SQL vs NoSQL specifically)
371
+ if (negationMismatch) {
372
+ return {
373
+ matches: false,
374
+ confidence: {
375
+ score: Math.min(score, 20),
376
+ method: 'semantic',
377
+ factors,
378
+ },
379
+ };
380
+ }
381
+ if (hasHardIncompatibility) {
382
+ return {
383
+ matches: false,
384
+ confidence: {
385
+ score: Math.min(score, 30),
386
+ method: 'semantic',
387
+ factors,
388
+ },
389
+ };
390
+ }
391
+ // IMPROVED MATCHING LOGIC (v1.3.0 - refined for precision):
392
+ // Match only if:
393
+ // 1. Exact category match with same severity (same finding)
394
+ // 2. Exact category match with similar severity (one-level difference is OK for paraphrases)
395
+ // 3. High synonym similarity (>= 60) with same tool
396
+ //
397
+ // DO NOT match if:
398
+ // - Severity difference of 2+ levels (high vs low = real drift, not paraphrase)
399
+ // - Different categories without very high confidence
400
+ const exactCategoryMatch = a.category === b.category && a.category !== 'other';
401
+ const relatedCategories = categoryScore >= 70; // Stricter threshold
402
+ const highDescriptionSimilarity = descSimilarity >= 40 || synonymSimilarity >= 50;
403
+ // For severity differences:
404
+ // - Exact match with any severity: always a match (paraphrases may describe severity differently)
405
+ // - Different categories: only match if very high similarity
406
+ //
407
+ // Note: We initially tried blocking 2+ level severity differences, but this hurt recall
408
+ // since the same vulnerability might be described with different severity assessments.
409
+ // Better to match and let human review decide if severity change is drift.
410
+ const matches = (exactCategoryMatch && toolMatch) ||
411
+ (exactCategoryMatch && descSimilarity >= 20) ||
412
+ (hasSharedSecurityTerms && toolMatch && synonymSimilarity >= 30) ||
413
+ (synonymSimilarity >= 60 && toolMatch) ||
414
+ (relatedCategories && highDescriptionSimilarity && toolMatch);
415
+ return {
416
+ matches,
417
+ confidence: {
418
+ score,
419
+ method: 'semantic',
420
+ factors,
421
+ },
422
+ };
423
+ }
424
+ /**
425
+ * Calculate similarity between severity levels.
426
+ */
427
+ function severityDistance(a, b) {
428
+ const levels = { low: 0, medium: 1, high: 2, critical: 3 };
429
+ const distance = Math.abs(levels[a] - levels[b]);
430
+ // 0 distance = 100%, 1 = 66%, 2 = 33%, 3 = 0%
431
+ return Math.round(100 - (distance / 3) * 100);
432
+ }
433
+ /**
434
+ * Compare two structured limitations.
435
+ * Returns true if they represent the same limitation.
436
+ */
437
+ export function limitationsMatch(a, b) {
438
+ return (a.category === b.category &&
439
+ a.tool === b.tool
440
+ // Note: We don't compare constraint since "10MB" vs "10 MB" would fail
441
+ );
442
+ }
443
+ /**
444
+ * Compare two structured limitations with confidence.
445
+ * Returns a confidence score indicating how similar they are.
446
+ *
447
+ * ENHANCED (v1.1.0): Uses multi-category detection and relationship scoring
448
+ * to improve recall for limitation paraphrases.
449
+ *
450
+ * ENHANCED (v1.2.0): Added qualifier comparison to prevent false positives from:
451
+ * - Direction mismatches (upload limit vs download limit)
452
+ * - Timeout type mismatches (connection timeout vs read timeout)
453
+ * - Rate time unit mismatches (per minute vs per hour)
454
+ *
455
+ * ENHANCED (v1.3.0): Improved recall by:
456
+ * - Adding synonym-based similarity for limitation descriptions
457
+ * - Time expression normalization (30s = 30 seconds)
458
+ * - Relaxed matching thresholds while maintaining constraint validation
459
+ *
460
+ * IMPORTANT: Two limitations with the same category but significantly different
461
+ * constraint values (e.g., 10MB vs 100MB) are NOT considered matching.
462
+ */
463
+ export function limitationsMatchWithConfidence(a, b) {
464
+ const factors = [];
465
+ // Expand abbreviations for better matching
466
+ const descA = expandAbbreviations(a.description);
467
+ const descB = expandAbbreviations(b.description);
468
+ // Check qualifier compatibility (upload vs download, timeout types, rate units)
469
+ const qualifierComparison = compareQualifiers(a.description, b.description);
470
+ // Only strict qualifier incompatibilities should block matching
471
+ const hasHardIncompatibility = qualifierComparison.incompatibilities.some(inc => inc.includes('upload vs download') || inc.includes('connection timeout vs read timeout'));
472
+ if (qualifierComparison.incompatibilities.length > 0) {
473
+ factors.push({
474
+ name: 'qualifier_compatibility',
475
+ weight: 0.1,
476
+ value: qualifierComparison.score,
477
+ description: `Qualifier issues: ${qualifierComparison.incompatibilities.join(', ')}`,
478
+ });
479
+ }
480
+ // Check time expression equivalence (30 seconds = 30s = 30000ms)
481
+ const timeExpressionsMatch = timeExpressionsEqual(descA, descB);
482
+ if (timeExpressionsMatch) {
483
+ factors.push({
484
+ name: 'time_equivalence',
485
+ weight: 0.15,
486
+ value: 100,
487
+ description: 'Time expressions are equivalent',
488
+ });
489
+ }
490
+ // Synonym-based similarity for limitations
491
+ const synonymSimilarity = calculateSynonymSimilarity(descA, descB, 'limitation');
492
+ if (synonymSimilarity > 0) {
493
+ factors.push({
494
+ name: 'synonym_similarity',
495
+ weight: 0.15,
496
+ value: synonymSimilarity,
497
+ description: `${synonymSimilarity}% synonym-based similarity`,
498
+ });
499
+ }
500
+ // Multi-category detection for limitations
501
+ const categoriesA = extractLimitationCategories(descA);
502
+ const categoriesB = extractLimitationCategories(descB);
503
+ // Find best matching category pair
504
+ const bestCategoryMatch = findBestLimitationCategoryMatch(categoriesA, categoriesB);
505
+ // Calculate category relationship score
506
+ let categoryScore;
507
+ let categoryDescription;
508
+ if (a.category === b.category) {
509
+ categoryScore = 100;
510
+ categoryDescription = `Categories match exactly: ${a.category}`;
511
+ }
512
+ else if (bestCategoryMatch && bestCategoryMatch.relationshipScore >= 60) {
513
+ categoryScore = bestCategoryMatch.relationshipScore;
514
+ categoryDescription = `Related categories: ${bestCategoryMatch.cat1} ~ ${bestCategoryMatch.cat2} (${bestCategoryMatch.relationshipScore}% related)`;
515
+ }
516
+ else if (synonymSimilarity >= 50) {
517
+ // Synonym similarity suggests same limitation type
518
+ categoryScore = 70;
519
+ categoryDescription = `Categories inferred from synonym similarity: ${synonymSimilarity}%`;
520
+ }
521
+ else {
522
+ const directRelationship = calculateLimitationCategoryRelationship(a.category, b.category);
523
+ categoryScore = Math.max(directRelationship, 20); // Minimum 20 for any limitation
524
+ categoryDescription = directRelationship >= 50
525
+ ? `Related categories: ${a.category} ~ ${b.category} (${directRelationship}% related)`
526
+ : `Categories differ: ${a.category} vs ${b.category}`;
527
+ }
528
+ // Category match (25% weight - reduced to make room for synonyms)
529
+ factors.push({
530
+ name: 'category_match',
531
+ weight: 0.25,
532
+ value: categoryScore,
533
+ description: categoryDescription,
534
+ });
535
+ // Tool match (10% weight)
536
+ const toolMatch = a.tool === b.tool;
537
+ factors.push({
538
+ name: 'tool_match',
539
+ weight: 0.1,
540
+ value: toolMatch ? 100 : 50,
541
+ description: toolMatch
542
+ ? `Tools match: ${a.tool}`
543
+ : `Different tools: ${a.tool} vs ${b.tool}`,
544
+ });
545
+ // Constraint similarity (20% weight - reduced slightly)
546
+ // Also check if time expressions match even if constraints don't
547
+ let constraintScore = constraintsMatch(a.constraint, b.constraint);
548
+ if (timeExpressionsMatch && constraintScore < 100) {
549
+ constraintScore = Math.max(constraintScore, 90); // Time equivalence implies constraint match
550
+ }
551
+ factors.push({
552
+ name: 'constraint_match',
553
+ weight: 0.2,
554
+ value: constraintScore,
555
+ description: constraintScore === 100
556
+ ? 'Constraints match exactly'
557
+ : constraintScore > 80
558
+ ? 'Constraints very similar'
559
+ : constraintScore > 50
560
+ ? 'Constraints similar'
561
+ : 'Constraints differ significantly',
562
+ });
563
+ // Description similarity with stemming
564
+ const descSimilarity = calculateStemmedKeywordOverlap(descA, descB);
565
+ factors.push({
566
+ name: 'description_similarity',
567
+ weight: 0.15,
568
+ value: descSimilarity,
569
+ description: `${descSimilarity}% description keyword overlap (stemmed)`,
570
+ });
571
+ const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
572
+ const score = Math.round(factors.reduce((sum, f) => sum + f.weight * f.value, 0) / totalWeight);
573
+ // CRITICAL: Block matching for semantic conflicts
574
+ // For limitations, qualifier incompatibilities should block matching
575
+ // (e.g., "no limit" vs "limit of", "per minute" vs "per hour")
576
+ if (hasHardIncompatibility || qualifierComparison.incompatibilities.length > 0) {
577
+ return {
578
+ matches: false,
579
+ confidence: {
580
+ score: Math.min(score, 35),
581
+ method: 'semantic',
582
+ factors,
583
+ },
584
+ };
585
+ }
586
+ // IMPROVED MATCHING LOGIC (v1.3.0 - balanced for precision/recall):
587
+ // Match if:
588
+ // 1. Exact category match with compatible constraints
589
+ // 2. Time expressions are equivalent (implies same limitation)
590
+ // 3. Same category with moderate description similarity
591
+ // 4. High synonym similarity with same tool
592
+ //
593
+ // IMPORTANT: Constraint compatibility is still required (but threshold lowered)
594
+ const exactCategoryMatch = a.category === b.category && a.category !== 'other';
595
+ const constraintsCompatible = constraintScore > 35 || timeExpressionsMatch;
596
+ const moderateDescriptionSimilarity = descSimilarity >= 35 || synonymSimilarity >= 40;
597
+ // Constraints must be compatible for limitations to match
598
+ const matches = constraintsCompatible && ((exactCategoryMatch) ||
599
+ timeExpressionsMatch ||
600
+ (toolMatch && moderateDescriptionSimilarity && synonymSimilarity >= 30));
601
+ return {
602
+ matches,
603
+ confidence: {
604
+ score,
605
+ method: 'semantic',
606
+ factors,
607
+ },
608
+ };
609
+ }
610
+ /**
611
+ * Compare constraint values, handling variations like "10MB" vs "10 MB".
612
+ * Now uses enhanced constraint comparison with unit normalization (e.g., 10MB = 10240KB).
613
+ */
614
+ function constraintsMatch(a, b) {
615
+ // Use the enhanced constraint comparison that handles unit conversions
616
+ return compareConstraints(a, b);
617
+ }
618
+ /**
619
+ * Compare two normalized assertions.
620
+ * Returns true if they have the same fingerprint.
621
+ */
622
+ export function assertionsMatch(a, b) {
623
+ return a.fingerprint === b.fingerprint;
624
+ }
625
+ /**
626
+ * Compare two normalized assertions with confidence.
627
+ * Returns a confidence score indicating how similar they are.
628
+ *
629
+ * ENHANCED (v1.2.0): Added qualifier comparison to prevent false positives from:
630
+ * - Opposite terms (synchronous vs asynchronous, enabled vs disabled)
631
+ * - Status code differences (200 vs 201)
632
+ *
633
+ * ENHANCED (v1.3.0): Improved recall by:
634
+ * - Adding synonym-based similarity for behavioral descriptions
635
+ * - Relaxed fingerprint matching (partial matches now count)
636
+ * - Better polarity detection that handles paraphrasing
637
+ * - Lower thresholds while blocking only clear semantic conflicts
638
+ */
639
+ export function assertionsMatchWithConfidence(a, b) {
640
+ const factors = [];
641
+ // Expand abbreviations for better matching
642
+ const descA = expandAbbreviations(a.description);
643
+ const descB = expandAbbreviations(b.description);
644
+ // Check qualifier compatibility (opposite terms, negation)
645
+ const qualifierComparison = compareQualifiers(a.description, b.description);
646
+ // Only hard incompatibilities should block matching
647
+ const hasHardIncompatibility = qualifierComparison.incompatibilities.some(inc => inc.includes('synchronous vs asynchronous') || inc.includes('enabled vs disabled'));
648
+ if (qualifierComparison.incompatibilities.length > 0) {
649
+ factors.push({
650
+ name: 'qualifier_compatibility',
651
+ weight: 0.1,
652
+ value: qualifierComparison.score,
653
+ description: `Qualifier issues: ${qualifierComparison.incompatibilities.join(', ')}`,
654
+ });
655
+ }
656
+ // Synonym-based similarity for behavioral descriptions
657
+ const synonymSimilarity = calculateSynonymSimilarity(descA, descB, 'behavior');
658
+ if (synonymSimilarity > 0) {
659
+ factors.push({
660
+ name: 'synonym_similarity',
661
+ weight: 0.15,
662
+ value: synonymSimilarity,
663
+ description: `${synonymSimilarity}% synonym-based similarity`,
664
+ });
665
+ }
666
+ // Fingerprint match (25% weight - reduced to allow more flexibility)
667
+ const fingerprintMatch = a.fingerprint === b.fingerprint;
668
+ const fpSimilarity = fingerprintSimilarity(a.fingerprint, b.fingerprint);
669
+ factors.push({
670
+ name: 'fingerprint_match',
671
+ weight: 0.25,
672
+ value: fingerprintMatch ? 100 : fpSimilarity,
673
+ description: fingerprintMatch
674
+ ? 'Fingerprints match exactly'
675
+ : `Fingerprints ${fpSimilarity}% similar`,
676
+ });
677
+ // Tool and aspect match (20% weight)
678
+ const toolAspectMatch = a.tool === b.tool && a.aspect === b.aspect;
679
+ const toolMatch = a.tool === b.tool;
680
+ factors.push({
681
+ name: 'tool_aspect_match',
682
+ weight: 0.2,
683
+ value: toolAspectMatch ? 100 : (toolMatch ? 70 : 30),
684
+ description: toolAspectMatch
685
+ ? `Tool and aspect match: ${a.tool}/${a.aspect}`
686
+ : toolMatch
687
+ ? `Tool matches: ${a.tool}, aspects differ`
688
+ : `Tools differ`,
689
+ });
690
+ // Polarity match (10% weight)
691
+ // IMPROVED: Handle paraphrases where polarity is implicitly the same
692
+ const polarityMatch = a.isPositive === b.isPositive;
693
+ // If descriptions are highly similar, assume polarity is consistent (paraphrase)
694
+ const implicitPolarityMatch = synonymSimilarity >= 60 || fpSimilarity >= 70;
695
+ factors.push({
696
+ name: 'polarity_match',
697
+ weight: 0.1,
698
+ value: polarityMatch ? 100 : (implicitPolarityMatch ? 80 : 20),
699
+ description: polarityMatch
700
+ ? 'Same polarity'
701
+ : implicitPolarityMatch
702
+ ? 'Polarity difference likely paraphrasing'
703
+ : 'Different polarity (positive/negative)',
704
+ });
705
+ // Description similarity with stemming
706
+ const descSimilarity = calculateStemmedKeywordOverlap(descA, descB);
707
+ factors.push({
708
+ name: 'description_similarity',
709
+ weight: 0.2,
710
+ value: descSimilarity,
711
+ description: `${descSimilarity}% description keyword overlap (stemmed)`,
712
+ });
713
+ const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
714
+ const score = Math.round(factors.reduce((sum, f) => sum + f.weight * f.value, 0) / totalWeight);
715
+ // CRITICAL: Block matching for semantic conflicts
716
+ // For assertions, ANY qualifier incompatibility should block matching
717
+ // (assertions define precise behaviors - "error" vs "null" is real drift)
718
+ if (hasHardIncompatibility || qualifierComparison.incompatibilities.length > 0) {
719
+ return {
720
+ matches: false,
721
+ confidence: {
722
+ score: Math.min(score, 40),
723
+ method: 'semantic',
724
+ factors,
725
+ },
726
+ };
727
+ }
728
+ // IMPROVED MATCHING LOGIC (v1.3.0 - balanced for precision/recall):
729
+ // Match if:
730
+ // 1. Exact fingerprint match (almost certainly same assertion)
731
+ // 2. Tool/aspect match with moderate description similarity AND same polarity
732
+ // 3. High fingerprint similarity (>= 60) with matching tool
733
+ // 4. High synonym similarity (>= 50) with tool match
734
+ const highFingerprintSimilarity = fpSimilarity >= 60;
735
+ const moderateDescriptionSimilarity = descSimilarity >= 40 || synonymSimilarity >= 45;
736
+ const matches = fingerprintMatch ||
737
+ (toolAspectMatch && moderateDescriptionSimilarity && polarityMatch) ||
738
+ (toolMatch && highFingerprintSimilarity && polarityMatch) ||
739
+ (toolMatch && synonymSimilarity >= 50 && polarityMatch);
740
+ return {
741
+ matches,
742
+ confidence: {
743
+ score,
744
+ method: 'semantic',
745
+ factors,
746
+ },
747
+ };
748
+ }
749
+ /**
750
+ * Calculate similarity between two fingerprints.
751
+ */
752
+ function fingerprintSimilarity(a, b) {
753
+ const partsA = new Set(a.split(':'));
754
+ const partsB = new Set(b.split(':'));
755
+ if (partsA.size === 0 && partsB.size === 0)
756
+ return 100;
757
+ if (partsA.size === 0 || partsB.size === 0)
758
+ return 0;
759
+ const intersection = new Set([...partsA].filter((p) => partsB.has(p)));
760
+ const union = new Set([...partsA, ...partsB]);
761
+ return Math.round((intersection.size / union.size) * 100);
762
+ }
763
+ /**
764
+ * Find matching item in array using matcher function.
765
+ */
766
+ export function findMatch(item, array, matcher) {
767
+ return array.find(other => matcher(item, other));
768
+ }
769
+ /**
770
+ * Compare two arrays using semantic matching.
771
+ * Returns items that are only in first array (removed) and only in second (added).
772
+ */
773
+ export function compareArraysSemantic(previous, current, matcher) {
774
+ const added = [];
775
+ const removed = [];
776
+ // Find removed (in previous but not in current)
777
+ for (const prev of previous) {
778
+ if (!findMatch(prev, current, matcher)) {
779
+ removed.push(prev);
780
+ }
781
+ }
782
+ // Find added (in current but not in previous)
783
+ for (const curr of current) {
784
+ if (!findMatch(curr, previous, matcher)) {
785
+ added.push(curr);
786
+ }
787
+ }
788
+ return { added, removed };
789
+ }
790
+ /**
791
+ * Compare two arrays using semantic matching with confidence scores.
792
+ * Returns detailed comparison results including confidence for each item.
793
+ */
794
+ export function compareArraysSemanticWithConfidence(previous, current, matcherWithConfidence) {
795
+ const added = [];
796
+ const removed = [];
797
+ const matched = [];
798
+ const matchedCurrentIndices = new Set();
799
+ // For each previous item, find best match in current
800
+ for (const prev of previous) {
801
+ let bestMatch = null;
802
+ for (let i = 0; i < current.length; i++) {
803
+ if (matchedCurrentIndices.has(i))
804
+ continue;
805
+ const result = matcherWithConfidence(prev, current[i]);
806
+ if (result.matches) {
807
+ if (!bestMatch || result.confidence.score > bestMatch.confidence.score) {
808
+ bestMatch = { index: i, current: current[i], confidence: result.confidence };
809
+ }
810
+ }
811
+ }
812
+ if (bestMatch) {
813
+ matchedCurrentIndices.add(bestMatch.index);
814
+ matched.push({
815
+ previous: prev,
816
+ current: bestMatch.current,
817
+ confidence: bestMatch.confidence,
818
+ });
819
+ }
820
+ else {
821
+ // Item was removed - calculate confidence that it's truly gone
822
+ removed.push({
823
+ item: prev,
824
+ confidence: {
825
+ score: 95, // High confidence for removals (we checked all items)
826
+ method: 'semantic',
827
+ factors: [
828
+ {
829
+ name: 'removal_check',
830
+ weight: 1.0,
831
+ value: 95,
832
+ description: `No matching item found in ${current.length} current items`,
833
+ },
834
+ ],
835
+ },
836
+ });
837
+ }
838
+ }
839
+ // Any unmatched current items are additions
840
+ for (let i = 0; i < current.length; i++) {
841
+ if (!matchedCurrentIndices.has(i)) {
842
+ added.push({
843
+ item: current[i],
844
+ confidence: {
845
+ score: 95, // High confidence for additions (we checked all items)
846
+ method: 'semantic',
847
+ factors: [
848
+ {
849
+ name: 'addition_check',
850
+ weight: 1.0,
851
+ value: 95,
852
+ description: `No matching item found in ${previous.length} previous items`,
853
+ },
854
+ ],
855
+ },
856
+ });
857
+ }
858
+ }
859
+ return { added, removed, matched };
860
+ }
861
+ /**
862
+ * Calculate overall confidence for a semantic comparison operation.
863
+ */
864
+ export function calculateComparisonConfidence(before, after, categoryMatch) {
865
+ const factors = [];
866
+ // Factor 1: Keyword overlap
867
+ const keywordScore = calculateKeywordOverlap(before, after);
868
+ factors.push({
869
+ name: 'keyword_overlap',
870
+ weight: CONFIDENCE_WEIGHTS.keywordOverlap,
871
+ value: keywordScore,
872
+ description: `${keywordScore}% keyword overlap between old and new text`,
873
+ });
874
+ // Factor 2: Length similarity
875
+ const lengthScore = calculateLengthSimilarity(before, after);
876
+ factors.push({
877
+ name: 'length_similarity',
878
+ weight: CONFIDENCE_WEIGHTS.structuralAlignment,
879
+ value: lengthScore,
880
+ description: `${lengthScore}% length similarity`,
881
+ });
882
+ // Factor 3: Semantic indicators
883
+ const semanticScore = calculateSemanticIndicators(before, after);
884
+ factors.push({
885
+ name: 'semantic_indicators',
886
+ weight: CONFIDENCE_WEIGHTS.semanticSimilarity,
887
+ value: semanticScore,
888
+ description: `${semanticScore}% semantic indicator match`,
889
+ });
890
+ // Factor 4: Category consistency
891
+ const categoryScore = categoryMatch ? 100 : 30;
892
+ factors.push({
893
+ name: 'category_consistency',
894
+ weight: CONFIDENCE_WEIGHTS.categoryConsistency,
895
+ value: categoryScore,
896
+ description: categoryMatch
897
+ ? 'Categories match between versions'
898
+ : 'Categories differ between versions',
899
+ });
900
+ const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
901
+ const score = Math.round(factors.reduce((sum, f) => sum + f.weight * f.value, 0) / totalWeight);
902
+ return {
903
+ score,
904
+ method: 'semantic',
905
+ factors,
906
+ };
907
+ }
908
+ //# sourceMappingURL=semantic.js.map