@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.
- package/CHANGELOG.md +291 -0
- package/LICENSE +21 -0
- package/README.md +739 -0
- package/dist/auth/credentials.d.ts +64 -0
- package/dist/auth/credentials.js +218 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/keychain.d.ts +64 -0
- package/dist/auth/keychain.js +268 -0
- package/dist/baseline/ab-testing.d.ts +80 -0
- package/dist/baseline/ab-testing.js +236 -0
- package/dist/baseline/ai-compatibility-scorer.d.ts +95 -0
- package/dist/baseline/ai-compatibility-scorer.js +606 -0
- package/dist/baseline/calibration.d.ts +77 -0
- package/dist/baseline/calibration.js +136 -0
- package/dist/baseline/category-matching.d.ts +85 -0
- package/dist/baseline/category-matching.js +289 -0
- package/dist/baseline/change-impact-analyzer.d.ts +98 -0
- package/dist/baseline/change-impact-analyzer.js +592 -0
- package/dist/baseline/comparator.d.ts +64 -0
- package/dist/baseline/comparator.js +916 -0
- package/dist/baseline/confidence.d.ts +55 -0
- package/dist/baseline/confidence.js +122 -0
- package/dist/baseline/converter.d.ts +61 -0
- package/dist/baseline/converter.js +585 -0
- package/dist/baseline/dependency-analyzer.d.ts +89 -0
- package/dist/baseline/dependency-analyzer.js +567 -0
- package/dist/baseline/deprecation-tracker.d.ts +133 -0
- package/dist/baseline/deprecation-tracker.js +322 -0
- package/dist/baseline/diff.d.ts +55 -0
- package/dist/baseline/diff.js +1584 -0
- package/dist/baseline/documentation-scorer.d.ts +205 -0
- package/dist/baseline/documentation-scorer.js +466 -0
- package/dist/baseline/embeddings.d.ts +118 -0
- package/dist/baseline/embeddings.js +251 -0
- package/dist/baseline/error-analyzer.d.ts +198 -0
- package/dist/baseline/error-analyzer.js +721 -0
- package/dist/baseline/evaluation/evaluator.d.ts +42 -0
- package/dist/baseline/evaluation/evaluator.js +323 -0
- package/dist/baseline/evaluation/expanded-dataset.d.ts +45 -0
- package/dist/baseline/evaluation/expanded-dataset.js +1164 -0
- package/dist/baseline/evaluation/golden-dataset.d.ts +58 -0
- package/dist/baseline/evaluation/golden-dataset.js +717 -0
- package/dist/baseline/evaluation/index.d.ts +15 -0
- package/dist/baseline/evaluation/index.js +15 -0
- package/dist/baseline/evaluation/types.d.ts +186 -0
- package/dist/baseline/evaluation/types.js +8 -0
- package/dist/baseline/external-dependency-detector.d.ts +181 -0
- package/dist/baseline/external-dependency-detector.js +524 -0
- package/dist/baseline/golden-output.d.ts +162 -0
- package/dist/baseline/golden-output.js +636 -0
- package/dist/baseline/health-scorer.d.ts +174 -0
- package/dist/baseline/health-scorer.js +451 -0
- package/dist/baseline/incremental-checker.d.ts +97 -0
- package/dist/baseline/incremental-checker.js +174 -0
- package/dist/baseline/index.d.ts +31 -0
- package/dist/baseline/index.js +42 -0
- package/dist/baseline/migration-generator.d.ts +137 -0
- package/dist/baseline/migration-generator.js +554 -0
- package/dist/baseline/migrations.d.ts +60 -0
- package/dist/baseline/migrations.js +197 -0
- package/dist/baseline/performance-tracker.d.ts +214 -0
- package/dist/baseline/performance-tracker.js +577 -0
- package/dist/baseline/pr-comment-generator.d.ts +117 -0
- package/dist/baseline/pr-comment-generator.js +546 -0
- package/dist/baseline/response-fingerprint.d.ts +127 -0
- package/dist/baseline/response-fingerprint.js +728 -0
- package/dist/baseline/response-schema-tracker.d.ts +129 -0
- package/dist/baseline/response-schema-tracker.js +420 -0
- package/dist/baseline/risk-scorer.d.ts +54 -0
- package/dist/baseline/risk-scorer.js +434 -0
- package/dist/baseline/saver.d.ts +89 -0
- package/dist/baseline/saver.js +554 -0
- package/dist/baseline/scenario-generator.d.ts +151 -0
- package/dist/baseline/scenario-generator.js +905 -0
- package/dist/baseline/schema-compare.d.ts +86 -0
- package/dist/baseline/schema-compare.js +557 -0
- package/dist/baseline/schema-evolution.d.ts +189 -0
- package/dist/baseline/schema-evolution.js +467 -0
- package/dist/baseline/semantic.d.ts +203 -0
- package/dist/baseline/semantic.js +908 -0
- package/dist/baseline/synonyms.d.ts +60 -0
- package/dist/baseline/synonyms.js +386 -0
- package/dist/baseline/telemetry.d.ts +165 -0
- package/dist/baseline/telemetry.js +294 -0
- package/dist/baseline/test-pruner.d.ts +120 -0
- package/dist/baseline/test-pruner.js +387 -0
- package/dist/baseline/types.d.ts +449 -0
- package/dist/baseline/types.js +5 -0
- package/dist/baseline/version.d.ts +138 -0
- package/dist/baseline/version.js +206 -0
- package/dist/cache/index.d.ts +5 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/response-cache.d.ts +151 -0
- package/dist/cache/response-cache.js +287 -0
- package/dist/ci/index.d.ts +60 -0
- package/dist/ci/index.js +342 -0
- package/dist/cli/commands/auth.d.ts +12 -0
- package/dist/cli/commands/auth.js +352 -0
- package/dist/cli/commands/badge.d.ts +3 -0
- package/dist/cli/commands/badge.js +74 -0
- package/dist/cli/commands/baseline-accept.d.ts +15 -0
- package/dist/cli/commands/baseline-accept.js +178 -0
- package/dist/cli/commands/baseline-migrate.d.ts +12 -0
- package/dist/cli/commands/baseline-migrate.js +164 -0
- package/dist/cli/commands/baseline.d.ts +14 -0
- package/dist/cli/commands/baseline.js +449 -0
- package/dist/cli/commands/beta.d.ts +10 -0
- package/dist/cli/commands/beta.js +231 -0
- package/dist/cli/commands/check.d.ts +11 -0
- package/dist/cli/commands/check.js +820 -0
- package/dist/cli/commands/cloud/badge.d.ts +3 -0
- package/dist/cli/commands/cloud/badge.js +74 -0
- package/dist/cli/commands/cloud/diff.d.ts +6 -0
- package/dist/cli/commands/cloud/diff.js +79 -0
- package/dist/cli/commands/cloud/history.d.ts +6 -0
- package/dist/cli/commands/cloud/history.js +102 -0
- package/dist/cli/commands/cloud/link.d.ts +9 -0
- package/dist/cli/commands/cloud/link.js +119 -0
- package/dist/cli/commands/cloud/login.d.ts +7 -0
- package/dist/cli/commands/cloud/login.js +499 -0
- package/dist/cli/commands/cloud/projects.d.ts +6 -0
- package/dist/cli/commands/cloud/projects.js +44 -0
- package/dist/cli/commands/cloud/shared.d.ts +7 -0
- package/dist/cli/commands/cloud/shared.js +42 -0
- package/dist/cli/commands/cloud/teams.d.ts +8 -0
- package/dist/cli/commands/cloud/teams.js +169 -0
- package/dist/cli/commands/cloud/upload.d.ts +8 -0
- package/dist/cli/commands/cloud/upload.js +181 -0
- package/dist/cli/commands/contract.d.ts +11 -0
- package/dist/cli/commands/contract.js +280 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.js +82 -0
- package/dist/cli/commands/eval.d.ts +9 -0
- package/dist/cli/commands/eval.js +187 -0
- package/dist/cli/commands/explore.d.ts +11 -0
- package/dist/cli/commands/explore.js +437 -0
- package/dist/cli/commands/feedback.d.ts +9 -0
- package/dist/cli/commands/feedback.js +174 -0
- package/dist/cli/commands/golden.d.ts +12 -0
- package/dist/cli/commands/golden.js +407 -0
- package/dist/cli/commands/history.d.ts +10 -0
- package/dist/cli/commands/history.js +202 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +219 -0
- package/dist/cli/commands/interview.d.ts +3 -0
- package/dist/cli/commands/interview.js +903 -0
- package/dist/cli/commands/link.d.ts +10 -0
- package/dist/cli/commands/link.js +169 -0
- package/dist/cli/commands/login.d.ts +7 -0
- package/dist/cli/commands/login.js +499 -0
- package/dist/cli/commands/preset.d.ts +33 -0
- package/dist/cli/commands/preset.js +297 -0
- package/dist/cli/commands/profile.d.ts +33 -0
- package/dist/cli/commands/profile.js +286 -0
- package/dist/cli/commands/registry.d.ts +11 -0
- package/dist/cli/commands/registry.js +146 -0
- package/dist/cli/commands/shared.d.ts +79 -0
- package/dist/cli/commands/shared.js +196 -0
- package/dist/cli/commands/teams.d.ts +8 -0
- package/dist/cli/commands/teams.js +169 -0
- package/dist/cli/commands/test.d.ts +9 -0
- package/dist/cli/commands/test.js +500 -0
- package/dist/cli/commands/upload.d.ts +8 -0
- package/dist/cli/commands/upload.js +223 -0
- package/dist/cli/commands/validate-config.d.ts +6 -0
- package/dist/cli/commands/validate-config.js +35 -0
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +283 -0
- package/dist/cli/commands/watch.d.ts +12 -0
- package/dist/cli/commands/watch.js +253 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +178 -0
- package/dist/cli/interactive.d.ts +47 -0
- package/dist/cli/interactive.js +216 -0
- package/dist/cli/output/terminal-reporter.d.ts +19 -0
- package/dist/cli/output/terminal-reporter.js +104 -0
- package/dist/cli/output.d.ts +226 -0
- package/dist/cli/output.js +438 -0
- package/dist/cli/utils/env.d.ts +5 -0
- package/dist/cli/utils/env.js +14 -0
- package/dist/cli/utils/progress.d.ts +59 -0
- package/dist/cli/utils/progress.js +206 -0
- package/dist/cli/utils/server-context.d.ts +10 -0
- package/dist/cli/utils/server-context.js +36 -0
- package/dist/cloud/auth.d.ts +144 -0
- package/dist/cloud/auth.js +374 -0
- package/dist/cloud/client.d.ts +24 -0
- package/dist/cloud/client.js +65 -0
- package/dist/cloud/http-client.d.ts +38 -0
- package/dist/cloud/http-client.js +215 -0
- package/dist/cloud/index.d.ts +23 -0
- package/dist/cloud/index.js +25 -0
- package/dist/cloud/mock-client.d.ts +107 -0
- package/dist/cloud/mock-client.js +545 -0
- package/dist/cloud/types.d.ts +515 -0
- package/dist/cloud/types.js +15 -0
- package/dist/config/defaults.d.ts +160 -0
- package/dist/config/defaults.js +169 -0
- package/dist/config/loader.d.ts +24 -0
- package/dist/config/loader.js +122 -0
- package/dist/config/template.d.ts +42 -0
- package/dist/config/template.js +647 -0
- package/dist/config/validator.d.ts +2112 -0
- package/dist/config/validator.js +658 -0
- package/dist/constants/cloud.d.ts +107 -0
- package/dist/constants/cloud.js +110 -0
- package/dist/constants/core.d.ts +521 -0
- package/dist/constants/core.js +556 -0
- package/dist/constants/testing.d.ts +1283 -0
- package/dist/constants/testing.js +1568 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/contract/index.js +5 -0
- package/dist/contract/validator.d.ts +177 -0
- package/dist/contract/validator.js +574 -0
- package/dist/cost/index.d.ts +6 -0
- package/dist/cost/index.js +5 -0
- package/dist/cost/tracker.d.ts +134 -0
- package/dist/cost/tracker.js +313 -0
- package/dist/discovery/discovery.d.ts +16 -0
- package/dist/discovery/discovery.js +173 -0
- package/dist/discovery/types.d.ts +51 -0
- package/dist/discovery/types.js +2 -0
- package/dist/docs/agents.d.ts +3 -0
- package/dist/docs/agents.js +995 -0
- package/dist/docs/contract.d.ts +51 -0
- package/dist/docs/contract.js +1681 -0
- package/dist/docs/generator.d.ts +4 -0
- package/dist/docs/generator.js +4 -0
- package/dist/docs/html-reporter.d.ts +9 -0
- package/dist/docs/html-reporter.js +757 -0
- package/dist/docs/index.d.ts +10 -0
- package/dist/docs/index.js +11 -0
- package/dist/docs/junit-reporter.d.ts +18 -0
- package/dist/docs/junit-reporter.js +210 -0
- package/dist/docs/report.d.ts +14 -0
- package/dist/docs/report.js +44 -0
- package/dist/docs/sarif-reporter.d.ts +19 -0
- package/dist/docs/sarif-reporter.js +335 -0
- package/dist/docs/shared.d.ts +35 -0
- package/dist/docs/shared.js +162 -0
- package/dist/docs/templates.d.ts +12 -0
- package/dist/docs/templates.js +76 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.js +6 -0
- package/dist/errors/retry.d.ts +92 -0
- package/dist/errors/retry.js +323 -0
- package/dist/errors/types.d.ts +321 -0
- package/dist/errors/types.js +584 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +32 -0
- package/dist/interview/dependency-resolver.d.ts +11 -0
- package/dist/interview/dependency-resolver.js +32 -0
- package/dist/interview/interviewer.d.ts +232 -0
- package/dist/interview/interviewer.js +1939 -0
- package/dist/interview/mock-response-generator.d.ts +7 -0
- package/dist/interview/mock-response-generator.js +102 -0
- package/dist/interview/orchestrator.d.ts +237 -0
- package/dist/interview/orchestrator.js +1296 -0
- package/dist/interview/rate-limiter.d.ts +15 -0
- package/dist/interview/rate-limiter.js +55 -0
- package/dist/interview/response-validator.d.ts +10 -0
- package/dist/interview/response-validator.js +132 -0
- package/dist/interview/schema-inferrer.d.ts +8 -0
- package/dist/interview/schema-inferrer.js +71 -0
- package/dist/interview/schema-test-generator.d.ts +71 -0
- package/dist/interview/schema-test-generator.js +834 -0
- package/dist/interview/smart-value-generator.d.ts +155 -0
- package/dist/interview/smart-value-generator.js +554 -0
- package/dist/interview/stateful-test-runner.d.ts +19 -0
- package/dist/interview/stateful-test-runner.js +106 -0
- package/dist/interview/types.d.ts +561 -0
- package/dist/interview/types.js +2 -0
- package/dist/llm/anthropic.d.ts +41 -0
- package/dist/llm/anthropic.js +355 -0
- package/dist/llm/client.d.ts +123 -0
- package/dist/llm/client.js +42 -0
- package/dist/llm/factory.d.ts +38 -0
- package/dist/llm/factory.js +145 -0
- package/dist/llm/fallback.d.ts +140 -0
- package/dist/llm/fallback.js +379 -0
- package/dist/llm/index.d.ts +18 -0
- package/dist/llm/index.js +15 -0
- package/dist/llm/ollama.d.ts +37 -0
- package/dist/llm/ollama.js +330 -0
- package/dist/llm/openai.d.ts +25 -0
- package/dist/llm/openai.js +320 -0
- package/dist/llm/token-budget.d.ts +161 -0
- package/dist/llm/token-budget.js +395 -0
- package/dist/logging/logger.d.ts +70 -0
- package/dist/logging/logger.js +130 -0
- package/dist/metrics/collector.d.ts +106 -0
- package/dist/metrics/collector.js +547 -0
- package/dist/metrics/index.d.ts +7 -0
- package/dist/metrics/index.js +7 -0
- package/dist/metrics/prometheus.d.ts +20 -0
- package/dist/metrics/prometheus.js +241 -0
- package/dist/metrics/types.d.ts +209 -0
- package/dist/metrics/types.js +5 -0
- package/dist/persona/builtins.d.ts +54 -0
- package/dist/persona/builtins.js +219 -0
- package/dist/persona/index.d.ts +8 -0
- package/dist/persona/index.js +8 -0
- package/dist/persona/loader.d.ts +30 -0
- package/dist/persona/loader.js +190 -0
- package/dist/persona/types.d.ts +144 -0
- package/dist/persona/types.js +5 -0
- package/dist/persona/validation.d.ts +94 -0
- package/dist/persona/validation.js +332 -0
- package/dist/prompts/index.d.ts +5 -0
- package/dist/prompts/index.js +5 -0
- package/dist/prompts/templates.d.ts +180 -0
- package/dist/prompts/templates.js +431 -0
- package/dist/registry/client.d.ts +49 -0
- package/dist/registry/client.js +191 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.js +6 -0
- package/dist/registry/types.d.ts +140 -0
- package/dist/registry/types.js +6 -0
- package/dist/scenarios/evaluator.d.ts +43 -0
- package/dist/scenarios/evaluator.js +206 -0
- package/dist/scenarios/index.d.ts +10 -0
- package/dist/scenarios/index.js +9 -0
- package/dist/scenarios/loader.d.ts +20 -0
- package/dist/scenarios/loader.js +285 -0
- package/dist/scenarios/types.d.ts +153 -0
- package/dist/scenarios/types.js +8 -0
- package/dist/security/index.d.ts +17 -0
- package/dist/security/index.js +18 -0
- package/dist/security/payloads.d.ts +61 -0
- package/dist/security/payloads.js +268 -0
- package/dist/security/security-tester.d.ts +42 -0
- package/dist/security/security-tester.js +582 -0
- package/dist/security/types.d.ts +166 -0
- package/dist/security/types.js +8 -0
- package/dist/transport/base-transport.d.ts +59 -0
- package/dist/transport/base-transport.js +38 -0
- package/dist/transport/http-transport.d.ts +67 -0
- package/dist/transport/http-transport.js +238 -0
- package/dist/transport/mcp-client.d.ts +141 -0
- package/dist/transport/mcp-client.js +496 -0
- package/dist/transport/sse-transport.d.ts +88 -0
- package/dist/transport/sse-transport.js +316 -0
- package/dist/transport/stdio-transport.d.ts +43 -0
- package/dist/transport/stdio-transport.js +238 -0
- package/dist/transport/types.d.ts +125 -0
- package/dist/transport/types.js +16 -0
- package/dist/utils/concurrency.d.ts +123 -0
- package/dist/utils/concurrency.js +213 -0
- package/dist/utils/formatters.d.ts +16 -0
- package/dist/utils/formatters.js +37 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/jsonpath.d.ts +87 -0
- package/dist/utils/jsonpath.js +326 -0
- package/dist/utils/markdown.d.ts +113 -0
- package/dist/utils/markdown.js +265 -0
- package/dist/utils/network.d.ts +14 -0
- package/dist/utils/network.js +17 -0
- package/dist/utils/sanitize.d.ts +92 -0
- package/dist/utils/sanitize.js +191 -0
- package/dist/utils/semantic.d.ts +194 -0
- package/dist/utils/semantic.js +1051 -0
- package/dist/utils/smart-truncate.d.ts +94 -0
- package/dist/utils/smart-truncate.js +361 -0
- package/dist/utils/timeout.d.ts +153 -0
- package/dist/utils/timeout.js +205 -0
- package/dist/utils/yaml-parser.d.ts +58 -0
- package/dist/utils/yaml-parser.js +86 -0
- package/dist/validation/index.d.ts +32 -0
- package/dist/validation/index.js +32 -0
- package/dist/validation/semantic-test-generator.d.ts +50 -0
- package/dist/validation/semantic-test-generator.js +176 -0
- package/dist/validation/semantic-types.d.ts +66 -0
- package/dist/validation/semantic-types.js +94 -0
- package/dist/validation/semantic-validator.d.ts +38 -0
- package/dist/validation/semantic-validator.js +340 -0
- package/dist/verification/index.d.ts +6 -0
- package/dist/verification/index.js +5 -0
- package/dist/verification/types.d.ts +133 -0
- package/dist/verification/types.js +5 -0
- package/dist/verification/verifier.d.ts +30 -0
- package/dist/verification/verifier.js +309 -0
- package/dist/version.d.ts +19 -0
- package/dist/version.js +48 -0
- package/dist/workflow/auto-generator.d.ts +27 -0
- package/dist/workflow/auto-generator.js +513 -0
- package/dist/workflow/discovery.d.ts +40 -0
- package/dist/workflow/discovery.js +195 -0
- package/dist/workflow/executor.d.ts +82 -0
- package/dist/workflow/executor.js +611 -0
- package/dist/workflow/index.d.ts +10 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/loader.d.ts +24 -0
- package/dist/workflow/loader.js +194 -0
- package/dist/workflow/state-tracker.d.ts +98 -0
- package/dist/workflow/state-tracker.js +424 -0
- package/dist/workflow/types.d.ts +337 -0
- package/dist/workflow/types.js +5 -0
- package/package.json +94 -0
- 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
|