@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,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response fingerprinting for structural drift detection.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes MCP tool responses to create deterministic fingerprints
|
|
5
|
+
* that capture response structure, shape, and characteristics without
|
|
6
|
+
* requiring LLM analysis.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { analyzeErrorPatterns as analyzeErrorPatternsEnhanced } from './error-analyzer.js';
|
|
10
|
+
/**
|
|
11
|
+
* Detect if content appears to be binary data.
|
|
12
|
+
* Checks for Base64 data URIs, control characters, and other binary patterns.
|
|
13
|
+
*/
|
|
14
|
+
function isBinaryContent(content) {
|
|
15
|
+
if (typeof content !== 'string')
|
|
16
|
+
return false;
|
|
17
|
+
// Sample only the first portion to avoid performance issues with large strings
|
|
18
|
+
const sample = content.slice(0, 1000);
|
|
19
|
+
// Check for Base64 data URI pattern
|
|
20
|
+
if (/^data:[^;]+;base64,/.test(sample)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
// Check for control characters (excluding common whitespace)
|
|
24
|
+
// Control chars: 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F
|
|
25
|
+
// eslint-disable-next-line no-control-regex
|
|
26
|
+
if (/[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(sample)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
// Check for high concentration of non-printable characters
|
|
30
|
+
const nonPrintable = sample.match(/[^\x20-\x7E\t\n\r]/g);
|
|
31
|
+
if (nonPrintable && nonPrintable.length > sample.length * 0.3) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Categorize binary content size for fingerprinting.
|
|
38
|
+
*/
|
|
39
|
+
function categorizeBinarySize(content) {
|
|
40
|
+
const size = Buffer.byteLength(content, 'utf-8');
|
|
41
|
+
if (size < 1024)
|
|
42
|
+
return 'tiny';
|
|
43
|
+
if (size < 10 * 1024)
|
|
44
|
+
return 'small';
|
|
45
|
+
if (size < 100 * 1024)
|
|
46
|
+
return 'medium';
|
|
47
|
+
return 'large';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Analyze multiple tool responses to create a comprehensive fingerprint.
|
|
51
|
+
*/
|
|
52
|
+
export function analyzeResponses(responses) {
|
|
53
|
+
const successfulResponses = responses.filter((r) => r.response && !r.response.isError && !r.error);
|
|
54
|
+
const errorResponses = responses.filter((r) => r.error || r.response?.isError);
|
|
55
|
+
// Analyze successful responses
|
|
56
|
+
const structures = [];
|
|
57
|
+
const inferredSchemas = [];
|
|
58
|
+
for (const { response } of successfulResponses) {
|
|
59
|
+
if (!response)
|
|
60
|
+
continue;
|
|
61
|
+
const content = extractResponseContent(response);
|
|
62
|
+
if (content !== undefined) {
|
|
63
|
+
structures.push(computeStructureHash(content));
|
|
64
|
+
inferredSchemas.push(inferSchemaFromValue(content));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Analyze error patterns
|
|
68
|
+
const errorPatterns = analyzeErrorPatterns(errorResponses);
|
|
69
|
+
// Generate enhanced error analyses
|
|
70
|
+
const enhancedErrorAnalyses = errorPatterns.length > 0 ? analyzeErrorPatternsEnhanced(errorPatterns) : undefined;
|
|
71
|
+
// Build fingerprint
|
|
72
|
+
const fingerprint = buildFingerprint(successfulResponses, structures);
|
|
73
|
+
// Merge inferred schemas
|
|
74
|
+
const inferredSchema = inferredSchemas.length > 0 ? mergeSchemas(inferredSchemas) : undefined;
|
|
75
|
+
// Check consistency
|
|
76
|
+
const uniqueStructures = new Set(structures);
|
|
77
|
+
const isConsistent = uniqueStructures.size <= 1;
|
|
78
|
+
return {
|
|
79
|
+
fingerprint,
|
|
80
|
+
inferredSchema,
|
|
81
|
+
errorPatterns,
|
|
82
|
+
enhancedErrorAnalyses,
|
|
83
|
+
isConsistent,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract the meaningful content from an MCP tool response.
|
|
88
|
+
*/
|
|
89
|
+
function extractResponseContent(response) {
|
|
90
|
+
if (!response.content || response.content.length === 0) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
// Handle single content item
|
|
94
|
+
if (response.content.length === 1) {
|
|
95
|
+
const item = response.content[0];
|
|
96
|
+
if (item.type === 'text' && 'text' in item && typeof item.text === 'string') {
|
|
97
|
+
// Try to parse as JSON
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(item.text);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return item.text;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return item;
|
|
106
|
+
}
|
|
107
|
+
// Multiple content items - return as array
|
|
108
|
+
return response.content.map((item) => {
|
|
109
|
+
if (item.type === 'text' && 'text' in item && typeof item.text === 'string') {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(item.text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return item.text;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return item;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compute a structure hash that captures shape but not values.
|
|
122
|
+
*/
|
|
123
|
+
function computeStructureHash(value) {
|
|
124
|
+
const structure = extractStructure(value);
|
|
125
|
+
const serialized = JSON.stringify(structure);
|
|
126
|
+
return createHash('sha256').update(serialized).digest('hex').slice(0, 16);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract the structural representation of a value.
|
|
130
|
+
* Captures types, keys, and nesting but not actual values.
|
|
131
|
+
*/
|
|
132
|
+
function extractStructure(value, depth = 0) {
|
|
133
|
+
// Prevent infinite recursion
|
|
134
|
+
if (depth > 10) {
|
|
135
|
+
return { type: 'deep' };
|
|
136
|
+
}
|
|
137
|
+
if (value === null) {
|
|
138
|
+
return { type: 'null' };
|
|
139
|
+
}
|
|
140
|
+
if (value === undefined) {
|
|
141
|
+
return { type: 'undefined' };
|
|
142
|
+
}
|
|
143
|
+
const valueType = typeof value;
|
|
144
|
+
if (valueType === 'string') {
|
|
145
|
+
// Classify string patterns
|
|
146
|
+
const str = value;
|
|
147
|
+
if (str.length === 0)
|
|
148
|
+
return { type: 'string', subtype: 'empty' };
|
|
149
|
+
// Check for binary content before other patterns
|
|
150
|
+
if (isBinaryContent(str)) {
|
|
151
|
+
return {
|
|
152
|
+
type: 'binary',
|
|
153
|
+
size: categorizeBinarySize(str),
|
|
154
|
+
byteLength: Buffer.byteLength(str, 'utf-8'),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(str))
|
|
158
|
+
return { type: 'string', subtype: 'date' };
|
|
159
|
+
if (/^https?:\/\//.test(str))
|
|
160
|
+
return { type: 'string', subtype: 'url' };
|
|
161
|
+
if (/^[\w.-]+@[\w.-]+\.\w+$/.test(str))
|
|
162
|
+
return { type: 'string', subtype: 'email' };
|
|
163
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str)) {
|
|
164
|
+
return { type: 'string', subtype: 'uuid' };
|
|
165
|
+
}
|
|
166
|
+
return { type: 'string' };
|
|
167
|
+
}
|
|
168
|
+
if (valueType === 'number') {
|
|
169
|
+
const num = value;
|
|
170
|
+
if (Number.isInteger(num))
|
|
171
|
+
return { type: 'integer' };
|
|
172
|
+
return { type: 'number' };
|
|
173
|
+
}
|
|
174
|
+
if (valueType === 'boolean') {
|
|
175
|
+
return { type: 'boolean' };
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
if (value.length === 0) {
|
|
179
|
+
return { type: 'array', items: { type: 'unknown' }, empty: true };
|
|
180
|
+
}
|
|
181
|
+
// Sample first few items to determine array item structure
|
|
182
|
+
const sampleSize = Math.min(3, value.length);
|
|
183
|
+
const itemStructures = value
|
|
184
|
+
.slice(0, sampleSize)
|
|
185
|
+
.map((item) => extractStructure(item, depth + 1));
|
|
186
|
+
// Check if all items have the same structure
|
|
187
|
+
const firstStructure = JSON.stringify(itemStructures[0]);
|
|
188
|
+
const isHomogeneous = itemStructures.every((s) => JSON.stringify(s) === firstStructure);
|
|
189
|
+
return {
|
|
190
|
+
type: 'array',
|
|
191
|
+
items: isHomogeneous ? itemStructures[0] : { type: 'mixed' },
|
|
192
|
+
homogeneous: isHomogeneous,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (valueType === 'object') {
|
|
196
|
+
const obj = value;
|
|
197
|
+
const keys = Object.keys(obj).sort();
|
|
198
|
+
if (keys.length === 0) {
|
|
199
|
+
return { type: 'object', properties: {}, empty: true };
|
|
200
|
+
}
|
|
201
|
+
const properties = {};
|
|
202
|
+
for (const key of keys) {
|
|
203
|
+
properties[key] = extractStructure(obj[key], depth + 1);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties,
|
|
208
|
+
keys: keys.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { type: valueType };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Build a fingerprint from analyzed responses.
|
|
215
|
+
*/
|
|
216
|
+
function buildFingerprint(responses, structureHashes) {
|
|
217
|
+
if (responses.length === 0 || structureHashes.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
structureHash: 'empty',
|
|
220
|
+
contentType: 'empty',
|
|
221
|
+
size: 'tiny',
|
|
222
|
+
isEmpty: true,
|
|
223
|
+
sampleCount: 0,
|
|
224
|
+
confidence: 0,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Determine most common structure hash
|
|
228
|
+
const hashCounts = new Map();
|
|
229
|
+
for (const hash of structureHashes) {
|
|
230
|
+
hashCounts.set(hash, (hashCounts.get(hash) ?? 0) + 1);
|
|
231
|
+
}
|
|
232
|
+
let dominantHash = 'empty';
|
|
233
|
+
let maxCount = 0;
|
|
234
|
+
for (const [hash, count] of hashCounts) {
|
|
235
|
+
if (count > maxCount) {
|
|
236
|
+
dominantHash = hash;
|
|
237
|
+
maxCount = count;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Analyze first successful response for details
|
|
241
|
+
const firstResponse = responses.find((r) => r.response)?.response;
|
|
242
|
+
const content = firstResponse ? extractResponseContent(firstResponse) : undefined;
|
|
243
|
+
const contentType = classifyContentType(content);
|
|
244
|
+
const fields = extractTopLevelFields(content);
|
|
245
|
+
const arrayItemStructure = extractArrayItemStructure(content);
|
|
246
|
+
const size = classifySize(firstResponse);
|
|
247
|
+
const isEmpty = checkIsEmpty(content);
|
|
248
|
+
// Calculate confidence based on consistency
|
|
249
|
+
const confidence = structureHashes.length > 0 ? maxCount / structureHashes.length : 0;
|
|
250
|
+
return {
|
|
251
|
+
structureHash: dominantHash,
|
|
252
|
+
contentType,
|
|
253
|
+
fields,
|
|
254
|
+
arrayItemStructure,
|
|
255
|
+
size,
|
|
256
|
+
isEmpty,
|
|
257
|
+
sampleCount: responses.length,
|
|
258
|
+
confidence,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Classify the content type of a response.
|
|
263
|
+
*/
|
|
264
|
+
function classifyContentType(content) {
|
|
265
|
+
if (content === undefined || content === null) {
|
|
266
|
+
return 'empty';
|
|
267
|
+
}
|
|
268
|
+
if (typeof content === 'string') {
|
|
269
|
+
if (content.trim().length === 0)
|
|
270
|
+
return 'empty';
|
|
271
|
+
// Check for binary content
|
|
272
|
+
if (isBinaryContent(content))
|
|
273
|
+
return 'binary';
|
|
274
|
+
return 'text';
|
|
275
|
+
}
|
|
276
|
+
if (Array.isArray(content)) {
|
|
277
|
+
return 'array';
|
|
278
|
+
}
|
|
279
|
+
if (typeof content === 'object') {
|
|
280
|
+
return 'object';
|
|
281
|
+
}
|
|
282
|
+
if (typeof content === 'number' || typeof content === 'boolean') {
|
|
283
|
+
return 'primitive';
|
|
284
|
+
}
|
|
285
|
+
return 'mixed';
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Extract top-level field names from an object response.
|
|
289
|
+
*/
|
|
290
|
+
function extractTopLevelFields(content) {
|
|
291
|
+
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
292
|
+
return Object.keys(content).sort();
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Extract array item structure hash if content is an array.
|
|
298
|
+
*/
|
|
299
|
+
function extractArrayItemStructure(content) {
|
|
300
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
301
|
+
return computeStructureHash(content[0]);
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Classify response size.
|
|
307
|
+
*/
|
|
308
|
+
function classifySize(response) {
|
|
309
|
+
if (!response?.content)
|
|
310
|
+
return 'tiny';
|
|
311
|
+
let totalLength = 0;
|
|
312
|
+
for (const item of response.content) {
|
|
313
|
+
if (item.type === 'text' && 'text' in item && typeof item.text === 'string') {
|
|
314
|
+
totalLength += item.text.length;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (totalLength < 100)
|
|
318
|
+
return 'tiny';
|
|
319
|
+
if (totalLength < 1000)
|
|
320
|
+
return 'small';
|
|
321
|
+
if (totalLength < 10000)
|
|
322
|
+
return 'medium';
|
|
323
|
+
return 'large';
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Check if content is effectively empty.
|
|
327
|
+
*/
|
|
328
|
+
function checkIsEmpty(content) {
|
|
329
|
+
if (content === undefined || content === null)
|
|
330
|
+
return true;
|
|
331
|
+
if (typeof content === 'string') {
|
|
332
|
+
return content.trim().length === 0;
|
|
333
|
+
}
|
|
334
|
+
if (Array.isArray(content)) {
|
|
335
|
+
return content.length === 0;
|
|
336
|
+
}
|
|
337
|
+
if (typeof content === 'object') {
|
|
338
|
+
return Object.keys(content).length === 0;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Infer a JSON schema from a sample value.
|
|
344
|
+
*/
|
|
345
|
+
export function inferSchemaFromValue(value) {
|
|
346
|
+
if (value === null) {
|
|
347
|
+
return { type: 'null', nullable: true };
|
|
348
|
+
}
|
|
349
|
+
if (value === undefined) {
|
|
350
|
+
return { type: 'undefined', nullable: true };
|
|
351
|
+
}
|
|
352
|
+
const valueType = typeof value;
|
|
353
|
+
if (valueType === 'string') {
|
|
354
|
+
return { type: 'string' };
|
|
355
|
+
}
|
|
356
|
+
if (valueType === 'number') {
|
|
357
|
+
return { type: Number.isInteger(value) ? 'integer' : 'number' };
|
|
358
|
+
}
|
|
359
|
+
if (valueType === 'boolean') {
|
|
360
|
+
return { type: 'boolean' };
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
if (value.length === 0) {
|
|
364
|
+
return { type: 'array' };
|
|
365
|
+
}
|
|
366
|
+
// Infer item schema from samples
|
|
367
|
+
const itemSchemas = value.slice(0, 5).map(inferSchemaFromValue);
|
|
368
|
+
const mergedItemSchema = mergeSchemas(itemSchemas);
|
|
369
|
+
return {
|
|
370
|
+
type: 'array',
|
|
371
|
+
items: mergedItemSchema,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (valueType === 'object') {
|
|
375
|
+
const obj = value;
|
|
376
|
+
const properties = {};
|
|
377
|
+
const required = [];
|
|
378
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
379
|
+
properties[key] = inferSchemaFromValue(val);
|
|
380
|
+
if (val !== null && val !== undefined) {
|
|
381
|
+
required.push(key);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
type: 'object',
|
|
386
|
+
properties,
|
|
387
|
+
required: required.length > 0 ? required.sort() : undefined,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return { type: 'unknown' };
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Merge multiple inferred schemas into one.
|
|
394
|
+
*/
|
|
395
|
+
function mergeSchemas(schemas) {
|
|
396
|
+
if (schemas.length === 0) {
|
|
397
|
+
return { type: 'unknown' };
|
|
398
|
+
}
|
|
399
|
+
if (schemas.length === 1) {
|
|
400
|
+
return schemas[0];
|
|
401
|
+
}
|
|
402
|
+
// Check if all schemas have the same type
|
|
403
|
+
const types = new Set(schemas.map((s) => s.type));
|
|
404
|
+
if (types.size === 1) {
|
|
405
|
+
const type = schemas[0].type;
|
|
406
|
+
if (type === 'object') {
|
|
407
|
+
// Merge object properties
|
|
408
|
+
const allProperties = new Map();
|
|
409
|
+
const allRequiredSets = [];
|
|
410
|
+
for (const schema of schemas) {
|
|
411
|
+
if (schema.properties) {
|
|
412
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
413
|
+
if (!allProperties.has(key)) {
|
|
414
|
+
allProperties.set(key, []);
|
|
415
|
+
}
|
|
416
|
+
allProperties.get(key).push(propSchema);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (schema.required) {
|
|
420
|
+
allRequiredSets.push(new Set(schema.required));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const mergedProperties = {};
|
|
424
|
+
for (const [key, propSchemas] of allProperties) {
|
|
425
|
+
mergedProperties[key] = mergeSchemas(propSchemas);
|
|
426
|
+
}
|
|
427
|
+
// Required fields must be required in ALL schemas
|
|
428
|
+
let required;
|
|
429
|
+
if (allRequiredSets.length > 0) {
|
|
430
|
+
const intersection = allRequiredSets.reduce((acc, set) => {
|
|
431
|
+
return new Set([...acc].filter((x) => set.has(x)));
|
|
432
|
+
});
|
|
433
|
+
if (intersection.size > 0) {
|
|
434
|
+
required = [...intersection].sort();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
type: 'object',
|
|
439
|
+
properties: mergedProperties,
|
|
440
|
+
required,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (type === 'array' && schemas.every((s) => s.items)) {
|
|
444
|
+
// Merge array item schemas
|
|
445
|
+
const itemSchemas = schemas.map((s) => s.items);
|
|
446
|
+
return {
|
|
447
|
+
type: 'array',
|
|
448
|
+
items: mergeSchemas(itemSchemas),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return { type };
|
|
452
|
+
}
|
|
453
|
+
// Mixed types - return union-like
|
|
454
|
+
if (types.has('null') || types.has('undefined')) {
|
|
455
|
+
const nonNullSchemas = schemas.filter((s) => s.type !== 'null' && s.type !== 'undefined');
|
|
456
|
+
if (nonNullSchemas.length > 0) {
|
|
457
|
+
const merged = mergeSchemas(nonNullSchemas);
|
|
458
|
+
merged.nullable = true;
|
|
459
|
+
return merged;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { type: 'mixed' };
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Analyze error responses to extract patterns.
|
|
466
|
+
*/
|
|
467
|
+
function analyzeErrorPatterns(responses) {
|
|
468
|
+
const patterns = new Map();
|
|
469
|
+
for (const { response, error } of responses) {
|
|
470
|
+
const errorMessage = error ?? extractErrorMessage(response);
|
|
471
|
+
if (!errorMessage)
|
|
472
|
+
continue;
|
|
473
|
+
const category = categorizeError(errorMessage);
|
|
474
|
+
const patternHash = hashErrorPattern(errorMessage);
|
|
475
|
+
const key = `${category}:${patternHash}`;
|
|
476
|
+
if (patterns.has(key)) {
|
|
477
|
+
patterns.get(key).count++;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
patterns.set(key, {
|
|
481
|
+
category,
|
|
482
|
+
patternHash,
|
|
483
|
+
example: errorMessage.slice(0, 200),
|
|
484
|
+
count: 1,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return [...patterns.values()];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Extract error message from a response.
|
|
492
|
+
*/
|
|
493
|
+
function extractErrorMessage(response) {
|
|
494
|
+
if (!response?.isError)
|
|
495
|
+
return null;
|
|
496
|
+
const textContent = response.content?.find((c) => c.type === 'text');
|
|
497
|
+
if (textContent && 'text' in textContent && typeof textContent.text === 'string') {
|
|
498
|
+
return textContent.text;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Categorize an error message.
|
|
504
|
+
*/
|
|
505
|
+
function categorizeError(message) {
|
|
506
|
+
const lower = message.toLowerCase();
|
|
507
|
+
if (lower.includes('invalid') ||
|
|
508
|
+
lower.includes('required') ||
|
|
509
|
+
lower.includes('missing') ||
|
|
510
|
+
lower.includes('must be') ||
|
|
511
|
+
lower.includes('expected')) {
|
|
512
|
+
return 'validation';
|
|
513
|
+
}
|
|
514
|
+
if (lower.includes('not found') ||
|
|
515
|
+
lower.includes('does not exist') ||
|
|
516
|
+
lower.includes('no such') ||
|
|
517
|
+
lower.includes('404')) {
|
|
518
|
+
return 'not_found';
|
|
519
|
+
}
|
|
520
|
+
if (lower.includes('permission') ||
|
|
521
|
+
lower.includes('denied') ||
|
|
522
|
+
lower.includes('unauthorized') ||
|
|
523
|
+
lower.includes('forbidden') ||
|
|
524
|
+
lower.includes('access')) {
|
|
525
|
+
return 'permission';
|
|
526
|
+
}
|
|
527
|
+
if (lower.includes('timeout') || lower.includes('timed out')) {
|
|
528
|
+
return 'timeout';
|
|
529
|
+
}
|
|
530
|
+
if (lower.includes('internal') ||
|
|
531
|
+
lower.includes('server error') ||
|
|
532
|
+
lower.includes('unexpected')) {
|
|
533
|
+
return 'internal';
|
|
534
|
+
}
|
|
535
|
+
return 'unknown';
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Create a normalized hash of an error pattern.
|
|
539
|
+
* Strips specific values (IDs, paths, numbers) to capture the pattern.
|
|
540
|
+
*/
|
|
541
|
+
function hashErrorPattern(message) {
|
|
542
|
+
// Normalize the error message
|
|
543
|
+
const normalized = message
|
|
544
|
+
// Remove UUIDs
|
|
545
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
|
|
546
|
+
// Remove file paths
|
|
547
|
+
.replace(/\/[\w./\-_]+/g, '<PATH>')
|
|
548
|
+
// Remove numbers
|
|
549
|
+
.replace(/\b\d+\b/g, '<N>')
|
|
550
|
+
// Remove quoted strings
|
|
551
|
+
.replace(/"[^"]*"/g, '"<STR>"')
|
|
552
|
+
.replace(/'[^']*'/g, "'<STR>'")
|
|
553
|
+
// Normalize whitespace
|
|
554
|
+
.replace(/\s+/g, ' ')
|
|
555
|
+
.trim()
|
|
556
|
+
.toLowerCase();
|
|
557
|
+
return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Compare two response fingerprints.
|
|
561
|
+
*/
|
|
562
|
+
export function compareFingerprints(previous, current) {
|
|
563
|
+
// Handle missing fingerprints
|
|
564
|
+
if (!previous && !current) {
|
|
565
|
+
return { identical: true, changes: [], significance: 'none' };
|
|
566
|
+
}
|
|
567
|
+
if (!previous) {
|
|
568
|
+
return {
|
|
569
|
+
identical: false,
|
|
570
|
+
changes: [
|
|
571
|
+
{
|
|
572
|
+
aspect: 'structure',
|
|
573
|
+
description: 'Response fingerprint added (new baseline data)',
|
|
574
|
+
before: 'none',
|
|
575
|
+
after: current.structureHash,
|
|
576
|
+
breaking: false,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
significance: 'low',
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (!current) {
|
|
583
|
+
return {
|
|
584
|
+
identical: false,
|
|
585
|
+
changes: [
|
|
586
|
+
{
|
|
587
|
+
aspect: 'structure',
|
|
588
|
+
description: 'Response fingerprint removed',
|
|
589
|
+
before: previous.structureHash,
|
|
590
|
+
after: 'none',
|
|
591
|
+
breaking: false,
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
significance: 'low',
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const changes = [];
|
|
598
|
+
// Compare structure hash
|
|
599
|
+
if (previous.structureHash !== current.structureHash) {
|
|
600
|
+
changes.push({
|
|
601
|
+
aspect: 'structure',
|
|
602
|
+
description: 'Response structure changed',
|
|
603
|
+
before: previous.structureHash,
|
|
604
|
+
after: current.structureHash,
|
|
605
|
+
breaking: true,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
// Compare content type
|
|
609
|
+
if (previous.contentType !== current.contentType) {
|
|
610
|
+
changes.push({
|
|
611
|
+
aspect: 'content_type',
|
|
612
|
+
description: `Response type changed from ${previous.contentType} to ${current.contentType}`,
|
|
613
|
+
before: previous.contentType,
|
|
614
|
+
after: current.contentType,
|
|
615
|
+
breaking: true,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
// Compare fields
|
|
619
|
+
const prevFields = previous.fields?.join(',') ?? '';
|
|
620
|
+
const currFields = current.fields?.join(',') ?? '';
|
|
621
|
+
if (prevFields !== currFields) {
|
|
622
|
+
const addedFields = current.fields?.filter((f) => !previous.fields?.includes(f)) ?? [];
|
|
623
|
+
const removedFields = previous.fields?.filter((f) => !current.fields?.includes(f)) ?? [];
|
|
624
|
+
if (removedFields.length > 0) {
|
|
625
|
+
changes.push({
|
|
626
|
+
aspect: 'fields',
|
|
627
|
+
description: `Fields removed: ${removedFields.join(', ')}`,
|
|
628
|
+
before: prevFields,
|
|
629
|
+
after: currFields,
|
|
630
|
+
breaking: true,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (addedFields.length > 0) {
|
|
634
|
+
changes.push({
|
|
635
|
+
aspect: 'fields',
|
|
636
|
+
description: `Fields added: ${addedFields.join(', ')}`,
|
|
637
|
+
before: prevFields,
|
|
638
|
+
after: currFields,
|
|
639
|
+
breaking: false,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// Compare array item structure
|
|
644
|
+
if (previous.arrayItemStructure !== current.arrayItemStructure) {
|
|
645
|
+
changes.push({
|
|
646
|
+
aspect: 'array_items',
|
|
647
|
+
description: 'Array item structure changed',
|
|
648
|
+
before: previous.arrayItemStructure ?? 'none',
|
|
649
|
+
after: current.arrayItemStructure ?? 'none',
|
|
650
|
+
breaking: true,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// Compare emptiness (significant behavioral change)
|
|
654
|
+
if (previous.isEmpty !== current.isEmpty) {
|
|
655
|
+
changes.push({
|
|
656
|
+
aspect: 'emptiness',
|
|
657
|
+
description: previous.isEmpty
|
|
658
|
+
? 'Response now returns data (was empty)'
|
|
659
|
+
: 'Response now empty (was returning data)',
|
|
660
|
+
before: String(previous.isEmpty),
|
|
661
|
+
after: String(current.isEmpty),
|
|
662
|
+
breaking: !current.isEmpty, // Becoming empty is breaking
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
// Determine overall significance
|
|
666
|
+
let significance = 'none';
|
|
667
|
+
if (changes.length > 0) {
|
|
668
|
+
const hasBreaking = changes.some((c) => c.breaking);
|
|
669
|
+
const structureChanged = changes.some((c) => c.aspect === 'structure');
|
|
670
|
+
if (hasBreaking && structureChanged) {
|
|
671
|
+
significance = 'high';
|
|
672
|
+
}
|
|
673
|
+
else if (hasBreaking) {
|
|
674
|
+
significance = 'medium';
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
significance = 'low';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
identical: changes.length === 0,
|
|
682
|
+
changes,
|
|
683
|
+
significance,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
export function compareErrorPatterns(previous, current) {
|
|
687
|
+
const prevPatterns = new Set((previous ?? []).map((p) => p.patternHash));
|
|
688
|
+
const currPatterns = new Set((current ?? []).map((p) => p.patternHash));
|
|
689
|
+
const added = (current ?? []).filter((p) => !prevPatterns.has(p.patternHash));
|
|
690
|
+
const removed = (previous ?? []).filter((p) => !currPatterns.has(p.patternHash));
|
|
691
|
+
return {
|
|
692
|
+
added,
|
|
693
|
+
removed,
|
|
694
|
+
behaviorChanged: added.length > 0 || removed.length > 0,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Compute a hash for the inferred schema for comparison.
|
|
699
|
+
*/
|
|
700
|
+
export function computeInferredSchemaHash(schema) {
|
|
701
|
+
if (!schema)
|
|
702
|
+
return 'empty';
|
|
703
|
+
// Create normalized representation
|
|
704
|
+
const normalized = normalizeInferredSchema(schema);
|
|
705
|
+
const serialized = JSON.stringify(normalized);
|
|
706
|
+
return createHash('sha256').update(serialized).digest('hex').slice(0, 16);
|
|
707
|
+
}
|
|
708
|
+
function normalizeInferredSchema(schema) {
|
|
709
|
+
const result = { type: schema.type };
|
|
710
|
+
if (schema.nullable) {
|
|
711
|
+
result.nullable = true;
|
|
712
|
+
}
|
|
713
|
+
if (schema.properties) {
|
|
714
|
+
const sortedProps = {};
|
|
715
|
+
for (const key of Object.keys(schema.properties).sort()) {
|
|
716
|
+
sortedProps[key] = normalizeInferredSchema(schema.properties[key]);
|
|
717
|
+
}
|
|
718
|
+
result.properties = sortedProps;
|
|
719
|
+
}
|
|
720
|
+
if (schema.items) {
|
|
721
|
+
result.items = normalizeInferredSchema(schema.items);
|
|
722
|
+
}
|
|
723
|
+
if (schema.required && schema.required.length > 0) {
|
|
724
|
+
result.required = [...schema.required].sort();
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
//# sourceMappingURL=response-fingerprint.js.map
|