@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,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check command - Schema validation and drift detection for MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Fast, free, deterministic checking of MCP server contracts.
|
|
5
|
+
* Output: Documentation + JSON report (filenames configurable via output.files)
|
|
6
|
+
* Baseline: Full support (save, compare, diff)
|
|
7
|
+
* LLM: None required
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { MCPClient } from '../../transport/mcp-client.js';
|
|
13
|
+
import { discover } from '../../discovery/discovery.js';
|
|
14
|
+
import { Interviewer } from '../../interview/interviewer.js';
|
|
15
|
+
import { generateContractMd, generateJsonReport } from '../../docs/generator.js';
|
|
16
|
+
import { loadConfig, ConfigNotFoundError } from '../../config/loader.js';
|
|
17
|
+
import { validateConfigForCheck, getConfigWarnings } from '../../config/validator.js';
|
|
18
|
+
import { createBaseline, loadBaseline, saveBaseline, compareBaselines, acceptDrift, formatDiffText, formatDiffJson, formatDiffCompact, formatDiffGitHubActions, formatDiffMarkdown, formatDiffJUnit, formatDiffSarif, applySeverityConfig, shouldFailOnDiff, analyzeForIncremental, formatIncrementalSummary, runSecurityTests, parseSecurityCategories, getAllSecurityCategories, } from '../../baseline/index.js';
|
|
19
|
+
import { getMetricsCollector, resetMetricsCollector } from '../../metrics/collector.js';
|
|
20
|
+
import { getGlobalCache, resetGlobalCache } from '../../cache/response-cache.js';
|
|
21
|
+
import { InterviewProgressBar, formatCheckBanner } from '../utils/progress.js';
|
|
22
|
+
import { buildCheckSummary, colorizeConfidence, formatConfidenceLevel, formatToolResultLine, } from '../output/terminal-reporter.js';
|
|
23
|
+
import { loadScenariosFromFile, tryLoadDefaultScenarios, DEFAULT_SCENARIOS_FILE } from '../../scenarios/index.js';
|
|
24
|
+
import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE, WorkflowExecutor, generateWorkflowsFromTools, generateWorkflowYamlContent, } from '../../workflow/index.js';
|
|
25
|
+
import * as output from '../output.js';
|
|
26
|
+
import { extractServerContextFromArgs } from '../utils/server-context.js';
|
|
27
|
+
import { configureLogger } from '../../logging/logger.js';
|
|
28
|
+
import { EXIT_CODES, SEVERITY_TO_EXIT_CODE, PATHS, SECURITY_TESTING, CHECK_SAMPLING, WORKFLOW, REPORT_SCHEMAS, } from '../../constants.js';
|
|
29
|
+
export const checkCommand = new Command('check')
|
|
30
|
+
.description('Check MCP server schema and detect drift (free, fast, deterministic)')
|
|
31
|
+
.argument('[server-command]', 'Server command (overrides config)')
|
|
32
|
+
.argument('[args...]', 'Server arguments')
|
|
33
|
+
.option('-c, --config <path>', 'Path to config file', PATHS.DEFAULT_CONFIG_FILENAME)
|
|
34
|
+
.option('--fail-on-drift', 'Exit with error if drift detected (overrides config)')
|
|
35
|
+
.option('--accept-drift', 'Accept detected drift as intentional and update baseline')
|
|
36
|
+
.option('--accept-reason <reason>', 'Reason for accepting drift (used with --accept-drift)')
|
|
37
|
+
.option('--format <format>', 'Diff output format: text, json, compact, github, markdown, junit, sarif')
|
|
38
|
+
.option('--min-severity <level>', 'Minimum severity to report (overrides config): none, info, warning, breaking')
|
|
39
|
+
.option('--fail-on-severity <level>', 'Fail threshold (overrides config): none, info, warning, breaking')
|
|
40
|
+
.action(async (serverCommandArg, serverArgs, options) => {
|
|
41
|
+
// Load configuration
|
|
42
|
+
let config;
|
|
43
|
+
try {
|
|
44
|
+
config = loadConfig(options.config);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof ConfigNotFoundError) {
|
|
48
|
+
output.error(error.message);
|
|
49
|
+
process.exit(EXIT_CODES.ERROR);
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
// Determine server command (CLI arg overrides config)
|
|
54
|
+
const serverCommand = serverCommandArg || config.server.command;
|
|
55
|
+
const args = serverArgs.length > 0 ? serverArgs : config.server.args;
|
|
56
|
+
// Validate config for check
|
|
57
|
+
try {
|
|
58
|
+
validateConfigForCheck(config, serverCommand);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
output.error(error instanceof Error ? error.message : String(error));
|
|
62
|
+
process.exit(EXIT_CODES.ERROR);
|
|
63
|
+
}
|
|
64
|
+
const warnings = getConfigWarnings(config);
|
|
65
|
+
if (warnings.length > 0) {
|
|
66
|
+
output.warn('Configuration warnings:');
|
|
67
|
+
for (const warning of warnings) {
|
|
68
|
+
output.warn(` - ${warning}`);
|
|
69
|
+
}
|
|
70
|
+
output.newline();
|
|
71
|
+
}
|
|
72
|
+
// Extract settings from config
|
|
73
|
+
const timeout = config.server.timeout;
|
|
74
|
+
const outputDir = config.output.dir;
|
|
75
|
+
const docsDir = config.output.docsDir;
|
|
76
|
+
const cacheEnabled = config.cache.enabled;
|
|
77
|
+
const verbose = config.logging.verbose;
|
|
78
|
+
const logLevel = config.logging.level;
|
|
79
|
+
if (!process.env.BELLWETHER_LOG_OVERRIDE) {
|
|
80
|
+
// Configure logger based on config settings
|
|
81
|
+
// For CLI output, suppress internal pino logs unless verbose mode is on
|
|
82
|
+
// User-facing output uses the output module, not pino
|
|
83
|
+
const effectiveLogLevel = verbose ? logLevel : 'silent';
|
|
84
|
+
configureLogger({ level: effectiveLogLevel });
|
|
85
|
+
}
|
|
86
|
+
// Resolve baseline options from config (--fail-on-drift CLI flag can override)
|
|
87
|
+
const baselinePath = config.baseline.comparePath;
|
|
88
|
+
const saveBaselinePath = config.baseline.savePath;
|
|
89
|
+
const failOnDrift = options.failOnDrift ? true : config.baseline.failOnDrift;
|
|
90
|
+
// Build severity config (CLI options override config file)
|
|
91
|
+
const severityConfig = {
|
|
92
|
+
minimumSeverity: options.minSeverity ?? config.baseline.severity.minimumSeverity,
|
|
93
|
+
failOnSeverity: options.failOnSeverity ?? config.baseline.severity.failOnSeverity,
|
|
94
|
+
suppressWarnings: config.baseline.severity.suppressWarnings,
|
|
95
|
+
aspectOverrides: config.baseline.severity.aspectOverrides,
|
|
96
|
+
};
|
|
97
|
+
// Resolve check options from config (no CLI overrides for these)
|
|
98
|
+
const incrementalEnabled = config.check.incremental;
|
|
99
|
+
const incrementalCacheHours = config.check.incrementalCacheHours;
|
|
100
|
+
const parallelEnabled = config.check.parallel;
|
|
101
|
+
const parallelWorkers = config.check.parallelWorkers;
|
|
102
|
+
const performanceThreshold = config.check.performanceThreshold / 100;
|
|
103
|
+
const diffFormat = options.format ?? config.check.diffFormat;
|
|
104
|
+
// Resolve security options from config
|
|
105
|
+
const securityEnabled = config.check.security.enabled;
|
|
106
|
+
let securityCategories = config.check.security.categories;
|
|
107
|
+
// Validate security categories
|
|
108
|
+
try {
|
|
109
|
+
securityCategories = parseSecurityCategories(securityCategories.join(','));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
output.error(`Invalid security categories in config: ${error instanceof Error ? error.message : error}`);
|
|
113
|
+
output.info(`Valid categories: ${getAllSecurityCategories().join(', ')}`);
|
|
114
|
+
process.exit(EXIT_CODES.ERROR);
|
|
115
|
+
}
|
|
116
|
+
// Resolve sampling and confidence options from config
|
|
117
|
+
// Honor user's minSamples exactly - don't override with targetConfidence minimum
|
|
118
|
+
// If minSamples is below confidence threshold, the confidence level will reflect that
|
|
119
|
+
// but the user's choice is respected
|
|
120
|
+
const targetConfidence = config.check.sampling.targetConfidence;
|
|
121
|
+
const minSamples = config.check.sampling.minSamples;
|
|
122
|
+
const failOnLowConfidence = config.check.sampling.failOnLowConfidence;
|
|
123
|
+
// Resolve example output options from config
|
|
124
|
+
const fullExamples = config.output.examples.full;
|
|
125
|
+
const exampleLength = config.output.examples.maxLength;
|
|
126
|
+
const maxExamplesPerTool = config.output.examples.maxPerTool;
|
|
127
|
+
// Display startup banner
|
|
128
|
+
const banner = formatCheckBanner({
|
|
129
|
+
serverCommand: `${serverCommand} ${args.join(' ')}`,
|
|
130
|
+
});
|
|
131
|
+
output.info(banner);
|
|
132
|
+
output.newline();
|
|
133
|
+
output.info('Check: Schema validation and drift detection (free, deterministic)');
|
|
134
|
+
output.newline();
|
|
135
|
+
// Initialize metrics collector
|
|
136
|
+
resetMetricsCollector();
|
|
137
|
+
const metricsCollector = getMetricsCollector();
|
|
138
|
+
metricsCollector.startInterview();
|
|
139
|
+
// Initialize cache
|
|
140
|
+
resetGlobalCache();
|
|
141
|
+
const cache = getGlobalCache({ enabled: cacheEnabled });
|
|
142
|
+
if (cacheEnabled && verbose) {
|
|
143
|
+
output.info('Response caching enabled');
|
|
144
|
+
}
|
|
145
|
+
// Initialize MCP client
|
|
146
|
+
const mcpClient = new MCPClient({
|
|
147
|
+
timeout,
|
|
148
|
+
debug: logLevel === 'debug',
|
|
149
|
+
transport: 'stdio',
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
// Connect to MCP server
|
|
153
|
+
output.info('Connecting to MCP server...');
|
|
154
|
+
await mcpClient.connect(serverCommand, args, config.server.env);
|
|
155
|
+
// Discovery phase
|
|
156
|
+
output.info('Discovering capabilities...');
|
|
157
|
+
const discovery = await discover(mcpClient, serverCommand, args);
|
|
158
|
+
const resourceCount = discovery.resources?.length ?? 0;
|
|
159
|
+
const discoveryParts = [`${discovery.tools.length} tools`, `${discovery.prompts.length} prompts`];
|
|
160
|
+
if (resourceCount > 0) {
|
|
161
|
+
discoveryParts.push(`${resourceCount} resources`);
|
|
162
|
+
}
|
|
163
|
+
output.info(`Found ${discoveryParts.join(', ')}\n`);
|
|
164
|
+
// Update metrics
|
|
165
|
+
metricsCollector.updateInterviewCounters({
|
|
166
|
+
toolsDiscovered: discovery.tools.length,
|
|
167
|
+
personasUsed: 0, // No personas in check mode
|
|
168
|
+
});
|
|
169
|
+
if (discovery.tools.length === 0) {
|
|
170
|
+
output.info('No tools found. Nothing to check.');
|
|
171
|
+
metricsCollector.endInterview();
|
|
172
|
+
await mcpClient.disconnect();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Incremental checking - load baseline and determine which tools to test
|
|
176
|
+
let incrementalBaseline = null;
|
|
177
|
+
let incrementalResult = null;
|
|
178
|
+
if (incrementalEnabled) {
|
|
179
|
+
if (!baselinePath || !existsSync(baselinePath)) {
|
|
180
|
+
output.warn('Incremental mode requires a baseline. Testing all tools.');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
incrementalBaseline = loadBaseline(baselinePath);
|
|
184
|
+
incrementalResult = analyzeForIncremental(discovery.tools, incrementalBaseline, { maxCacheAgeHours: incrementalCacheHours });
|
|
185
|
+
const summary = formatIncrementalSummary(incrementalResult.changeSummary);
|
|
186
|
+
output.info(`Incremental analysis: ${summary}`);
|
|
187
|
+
if (incrementalResult.toolsToTest.length === 0) {
|
|
188
|
+
output.info('All tools unchanged. Using cached results.');
|
|
189
|
+
// Still need to generate output with cached data
|
|
190
|
+
// Skip to comparison section
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
output.info(`Testing ${incrementalResult.toolsToTest.length} tools (${incrementalResult.toolsToSkip.length} cached)\n`);
|
|
194
|
+
// Filter discovery to only include tools that need testing
|
|
195
|
+
discovery.tools = discovery.tools.filter(t => incrementalResult.toolsToTest.includes(t.name));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Load custom scenarios (work in check mode too)
|
|
200
|
+
let customScenarios;
|
|
201
|
+
if (config.scenarios.path) {
|
|
202
|
+
try {
|
|
203
|
+
customScenarios = loadScenariosFromFile(config.scenarios.path);
|
|
204
|
+
output.info(`Loaded ${customScenarios.toolScenarios.length} tool scenarios from ${config.scenarios.path}`);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
output.error(`Failed to load scenarios: ${error instanceof Error ? error.message : error}`);
|
|
208
|
+
process.exit(EXIT_CODES.ERROR);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const defaultScenarios = tryLoadDefaultScenarios(outputDir);
|
|
213
|
+
if (defaultScenarios) {
|
|
214
|
+
customScenarios = defaultScenarios;
|
|
215
|
+
output.info(`Auto-loaded ${customScenarios.toolScenarios.length} scenarios from ${DEFAULT_SCENARIOS_FILE}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Create interviewer for check mode (no LLM required)
|
|
219
|
+
const fullServerCommand = `${serverCommand} ${args.join(' ')}`.trim();
|
|
220
|
+
// Validate parallel workers (already resolved from config + CLI override)
|
|
221
|
+
let toolConcurrency = parallelWorkers;
|
|
222
|
+
if (toolConcurrency < 1) {
|
|
223
|
+
toolConcurrency = 4;
|
|
224
|
+
}
|
|
225
|
+
else if (toolConcurrency > 10) {
|
|
226
|
+
output.warn('Tool concurrency capped at 10');
|
|
227
|
+
toolConcurrency = 10;
|
|
228
|
+
}
|
|
229
|
+
if (parallelEnabled && !config.check.statefulTesting.enabled) {
|
|
230
|
+
output.info(`Parallel tool testing enabled (${toolConcurrency} workers)`);
|
|
231
|
+
}
|
|
232
|
+
if (securityEnabled) {
|
|
233
|
+
output.info(`Security testing enabled (${securityCategories.length} categories)`);
|
|
234
|
+
}
|
|
235
|
+
if (config.check.rateLimit.enabled) {
|
|
236
|
+
output.info(`Rate limiting enabled (${config.check.rateLimit.requestsPerSecond} req/s, burst ${config.check.rateLimit.burstLimit})`);
|
|
237
|
+
}
|
|
238
|
+
if (config.check.assertions.enabled) {
|
|
239
|
+
output.info(`Response assertions enabled (strict: ${config.check.assertions.strict ? 'on' : 'off'})`);
|
|
240
|
+
}
|
|
241
|
+
if (config.check.statefulTesting.enabled) {
|
|
242
|
+
output.info(`Stateful testing enabled (max chain length: ${config.check.statefulTesting.maxChainLength})`);
|
|
243
|
+
}
|
|
244
|
+
const interviewer = new Interviewer(null, {
|
|
245
|
+
maxQuestionsPerTool: minSamples, // Use configured min samples for test count
|
|
246
|
+
timeout,
|
|
247
|
+
skipErrorTests: false,
|
|
248
|
+
model: 'check', // Marker for check mode
|
|
249
|
+
// Note: personas defaults to DEFAULT_PERSONAS, which is needed for stats tracking
|
|
250
|
+
customScenarios,
|
|
251
|
+
customScenariosOnly: config.scenarios.only,
|
|
252
|
+
enableStreaming: false,
|
|
253
|
+
parallelPersonas: false,
|
|
254
|
+
parallelTools: parallelEnabled,
|
|
255
|
+
toolConcurrency,
|
|
256
|
+
cache,
|
|
257
|
+
checkMode: true, // Required when passing null for LLM
|
|
258
|
+
serverCommand: fullServerCommand,
|
|
259
|
+
warmupRuns: config.check.warmupRuns,
|
|
260
|
+
statefulTesting: config.check.statefulTesting,
|
|
261
|
+
externalServices: config.check.externalServices,
|
|
262
|
+
assertions: config.check.assertions,
|
|
263
|
+
rateLimit: config.check.rateLimit,
|
|
264
|
+
});
|
|
265
|
+
// Log sampling configuration
|
|
266
|
+
if (minSamples > CHECK_SAMPLING.DEFAULT_MIN_SAMPLES) {
|
|
267
|
+
output.info(`Sampling: ${minSamples} samples per tool (target confidence: ${targetConfidence})`);
|
|
268
|
+
}
|
|
269
|
+
// Extract server context
|
|
270
|
+
const serverContext = extractServerContextFromArgs(serverCommand, args);
|
|
271
|
+
if (serverContext.allowedDirectories && serverContext.allowedDirectories.length > 0) {
|
|
272
|
+
output.info(`Detected allowed directories: ${serverContext.allowedDirectories.join(', ')}`);
|
|
273
|
+
}
|
|
274
|
+
interviewer.setServerContext(serverContext);
|
|
275
|
+
// Set up progress display
|
|
276
|
+
const progressBar = new InterviewProgressBar({ enabled: !verbose });
|
|
277
|
+
const reportedTools = new Set();
|
|
278
|
+
const progressCallback = (progress) => {
|
|
279
|
+
if (verbose) {
|
|
280
|
+
switch (progress.phase) {
|
|
281
|
+
case 'starting':
|
|
282
|
+
output.info('Starting check...');
|
|
283
|
+
progressBar.start(progress.totalTools, 0, progress.totalPrompts ?? 0, progress.totalResources ?? 0);
|
|
284
|
+
break;
|
|
285
|
+
case 'interviewing':
|
|
286
|
+
output.info(`Checking: ${progress.currentTool} (${progress.toolsCompleted + 1}/${progress.totalTools})`);
|
|
287
|
+
break;
|
|
288
|
+
case 'complete':
|
|
289
|
+
output.info('Check complete!');
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
if (progress.phase === 'starting') {
|
|
295
|
+
progressBar.start(progress.totalTools, 0, progress.totalPrompts ?? 0, progress.totalResources ?? 0);
|
|
296
|
+
}
|
|
297
|
+
else if (['interviewing', 'prompts', 'resources'].includes(progress.phase)) {
|
|
298
|
+
progressBar.update(progress);
|
|
299
|
+
}
|
|
300
|
+
else if (progress.phase === 'complete') {
|
|
301
|
+
progressBar.stop();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const toolSummary = progress.lastCompletedTool;
|
|
305
|
+
if (toolSummary && !reportedTools.has(toolSummary.toolName)) {
|
|
306
|
+
const line = formatToolResultLine(toolSummary);
|
|
307
|
+
if (verbose) {
|
|
308
|
+
output.info(line);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
progressBar.log(line);
|
|
312
|
+
}
|
|
313
|
+
reportedTools.add(toolSummary.toolName);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
output.info('Checking schemas...\n');
|
|
317
|
+
const result = await interviewer.interview(mcpClient, discovery, progressCallback);
|
|
318
|
+
progressBar.stop();
|
|
319
|
+
if (!verbose) {
|
|
320
|
+
output.newline();
|
|
321
|
+
}
|
|
322
|
+
// Ensure output directories exist
|
|
323
|
+
mkdirSync(outputDir, { recursive: true });
|
|
324
|
+
if (docsDir !== outputDir) {
|
|
325
|
+
mkdirSync(docsDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
// End metrics (before security testing)
|
|
328
|
+
metricsCollector.endInterview();
|
|
329
|
+
output.info('\nCheck complete!');
|
|
330
|
+
output.info(`Duration: ${(result.metadata.durationMs / 1000).toFixed(1)}s`);
|
|
331
|
+
output.info(`Tools verified: ${result.toolProfiles.length}`);
|
|
332
|
+
// Display scenario results
|
|
333
|
+
if (result.scenarioResults && result.scenarioResults.length > 0) {
|
|
334
|
+
const passed = result.scenarioResults.filter((r) => r.passed).length;
|
|
335
|
+
const failed = result.scenarioResults.length - passed;
|
|
336
|
+
const statusIcon = failed === 0 ? '\u2713' : '\u2717';
|
|
337
|
+
output.info(`\nCustom scenarios: ${passed}/${result.scenarioResults.length} passed ${statusIcon}`);
|
|
338
|
+
if (failed > 0) {
|
|
339
|
+
output.info('\nFailed scenarios:');
|
|
340
|
+
for (const scenarioResult of result.scenarioResults.filter((r) => !r.passed)) {
|
|
341
|
+
const scenario = scenarioResult.scenario;
|
|
342
|
+
const toolOrPrompt = 'tool' in scenario ? scenario.tool : scenario.prompt;
|
|
343
|
+
output.info(` - ${toolOrPrompt}: ${scenario.description}`);
|
|
344
|
+
if (scenarioResult.error) {
|
|
345
|
+
output.info(` Error: ${scenarioResult.error}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// External service handling summary
|
|
351
|
+
if (result.metadata.externalServices) {
|
|
352
|
+
const ext = result.metadata.externalServices;
|
|
353
|
+
if (ext.unconfiguredServices.length > 0) {
|
|
354
|
+
output.warn(`\nExternal services not configured: ${ext.unconfiguredServices.join(', ')}`);
|
|
355
|
+
}
|
|
356
|
+
if (ext.skippedTools.length > 0) {
|
|
357
|
+
output.warn(`Tools skipped (${ext.skippedTools.length}): ${ext.skippedTools.slice(0, 5).join(', ')}${ext.skippedTools.length > 5 ? ' ...' : ''}`);
|
|
358
|
+
}
|
|
359
|
+
if (ext.mockedTools.length > 0) {
|
|
360
|
+
output.info(`Tools mocked (${ext.mockedTools.length}): ${ext.mockedTools.slice(0, 5).join(', ')}${ext.mockedTools.length > 5 ? ' ...' : ''}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (result.metadata.statefulTesting?.enabled) {
|
|
364
|
+
output.info(`\nStateful testing: ${result.metadata.statefulTesting.dependencyCount} dependency edge(s)`);
|
|
365
|
+
}
|
|
366
|
+
// Assertion summary
|
|
367
|
+
if (result.metadata.assertions && result.metadata.assertions.total > 0) {
|
|
368
|
+
const assertions = result.metadata.assertions;
|
|
369
|
+
if (assertions.failed > 0) {
|
|
370
|
+
output.warn(`\nResponse assertions failed: ${assertions.failed}/${assertions.total}`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
output.success(`\nResponse assertions: ${assertions.total} passed`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Rate limit summary
|
|
377
|
+
if (result.metadata.rateLimit) {
|
|
378
|
+
const rateLimit = result.metadata.rateLimit;
|
|
379
|
+
output.warn(`\nRate limit events: ${rateLimit.totalEvents} (retries: ${rateLimit.totalRetries})`);
|
|
380
|
+
if (rateLimit.tools.length > 0) {
|
|
381
|
+
output.info(`Rate-limited tools: ${rateLimit.tools.slice(0, 5).join(', ')}${rateLimit.tools.length > 5 ? ' ...' : ''}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const checkSummary = buildCheckSummary(result);
|
|
385
|
+
output.newline();
|
|
386
|
+
output.lines(...checkSummary.lines);
|
|
387
|
+
if (checkSummary.nextSteps.length > 0) {
|
|
388
|
+
output.newline();
|
|
389
|
+
output.info('Next steps:');
|
|
390
|
+
output.numberedList(checkSummary.nextSteps);
|
|
391
|
+
}
|
|
392
|
+
// Run security testing if enabled
|
|
393
|
+
const securityFingerprints = new Map();
|
|
394
|
+
if (securityEnabled) {
|
|
395
|
+
output.info('\n--- Security Testing ---');
|
|
396
|
+
output.info(`Testing categories: ${securityCategories.join(', ')}`);
|
|
397
|
+
output.newline();
|
|
398
|
+
let totalFindings = 0;
|
|
399
|
+
let criticalHighFindings = 0;
|
|
400
|
+
for (const toolProfile of result.toolProfiles) {
|
|
401
|
+
const tool = discovery.tools.find((t) => t.name === toolProfile.name);
|
|
402
|
+
if (!tool)
|
|
403
|
+
continue;
|
|
404
|
+
if (verbose) {
|
|
405
|
+
output.info(`Security testing: ${tool.name}`);
|
|
406
|
+
}
|
|
407
|
+
const fingerprint = await runSecurityTests({
|
|
408
|
+
toolName: tool.name,
|
|
409
|
+
toolDescription: tool.description || '',
|
|
410
|
+
inputSchema: tool.inputSchema,
|
|
411
|
+
callTool: async (args) => {
|
|
412
|
+
try {
|
|
413
|
+
const response = await mcpClient.callTool(tool.name, args);
|
|
414
|
+
const content = response.content
|
|
415
|
+
.map((c) => c.type === 'text' ? c.text : '')
|
|
416
|
+
.join('\n');
|
|
417
|
+
return {
|
|
418
|
+
isError: response.isError ?? false,
|
|
419
|
+
content,
|
|
420
|
+
errorMessage: response.isError ? content : undefined,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
isError: true,
|
|
426
|
+
content: '',
|
|
427
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
}, {
|
|
432
|
+
categories: securityCategories,
|
|
433
|
+
timeout: SECURITY_TESTING.TEST_TIMEOUT_MS,
|
|
434
|
+
maxPayloadsPerCategory: SECURITY_TESTING.MAX_PAYLOADS_PER_CATEGORY,
|
|
435
|
+
});
|
|
436
|
+
securityFingerprints.set(tool.name, fingerprint);
|
|
437
|
+
totalFindings += fingerprint.findings.length;
|
|
438
|
+
criticalHighFindings += fingerprint.findings.filter((f) => f.riskLevel === 'critical' || f.riskLevel === 'high').length;
|
|
439
|
+
if (verbose && fingerprint.findings.length > 0) {
|
|
440
|
+
for (const finding of fingerprint.findings) {
|
|
441
|
+
const color = finding.riskLevel === 'critical' || finding.riskLevel === 'high'
|
|
442
|
+
? output.error
|
|
443
|
+
: finding.riskLevel === 'medium'
|
|
444
|
+
? output.warn
|
|
445
|
+
: output.info;
|
|
446
|
+
color(` [${finding.riskLevel.toUpperCase()}] ${finding.title}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Summary
|
|
451
|
+
if (totalFindings > 0) {
|
|
452
|
+
output.warn(`Security testing complete: ${totalFindings} finding(s) detected`);
|
|
453
|
+
if (criticalHighFindings > 0) {
|
|
454
|
+
output.error(` ${criticalHighFindings} critical/high severity finding(s)`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
output.success('Security testing complete: No vulnerabilities detected');
|
|
459
|
+
}
|
|
460
|
+
output.newline();
|
|
461
|
+
}
|
|
462
|
+
// Workflow testing (stateful multi-step testing)
|
|
463
|
+
const workflowResults = [];
|
|
464
|
+
const workflowTimeout = config.workflows.stepTimeout;
|
|
465
|
+
const workflowTimeouts = config.workflows.timeouts;
|
|
466
|
+
// Load workflows from file or auto-discover from defaults
|
|
467
|
+
let workflows = [];
|
|
468
|
+
if (config.workflows.path) {
|
|
469
|
+
try {
|
|
470
|
+
workflows = loadWorkflowsFromFile(config.workflows.path);
|
|
471
|
+
output.info(`Loaded ${workflows.length} workflow(s) from ${config.workflows.path}`);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
output.error(`Failed to load workflows: ${error instanceof Error ? error.message : error}`);
|
|
475
|
+
process.exit(EXIT_CODES.ERROR);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Try to load default workflows
|
|
480
|
+
const defaultWorkflows = tryLoadDefaultWorkflows(outputDir);
|
|
481
|
+
if (defaultWorkflows) {
|
|
482
|
+
workflows = defaultWorkflows;
|
|
483
|
+
output.info(`Auto-loaded ${workflows.length} workflow(s) from ${DEFAULT_WORKFLOWS_FILE}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Generate workflows if configured
|
|
487
|
+
if (config.workflows.autoGenerate) {
|
|
488
|
+
const generatedPath = join(outputDir, DEFAULT_WORKFLOWS_FILE);
|
|
489
|
+
const existingWorkflows = workflows.length > 0;
|
|
490
|
+
const generated = generateWorkflowsFromTools(discovery.tools, {
|
|
491
|
+
maxWorkflows: WORKFLOW.MAX_DISCOVERED_WORKFLOWS,
|
|
492
|
+
minSteps: WORKFLOW.MIN_WORKFLOW_STEPS,
|
|
493
|
+
maxSteps: WORKFLOW.MAX_WORKFLOW_STEPS,
|
|
494
|
+
});
|
|
495
|
+
if (generated.length > 0) {
|
|
496
|
+
if (existingWorkflows) {
|
|
497
|
+
output.info(`Generated ${generated.length} additional workflow(s)`);
|
|
498
|
+
workflows = [...workflows, ...generated];
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
workflows = generated;
|
|
502
|
+
}
|
|
503
|
+
// Save generated workflows to file
|
|
504
|
+
const workflowYaml = generateWorkflowYamlContent(generated);
|
|
505
|
+
writeFileSync(generatedPath, workflowYaml);
|
|
506
|
+
output.info(`Generated workflow file: ${generatedPath}`);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
output.info('No workflows could be auto-generated from tool patterns');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Execute workflows if any are loaded
|
|
513
|
+
if (workflows.length > 0) {
|
|
514
|
+
output.info('\n--- Workflow Testing ---');
|
|
515
|
+
output.info(`Executing ${workflows.length} workflow(s)...\n`);
|
|
516
|
+
// Create a minimal executor for check mode (no LLM analysis)
|
|
517
|
+
const workflowExecutor = new WorkflowExecutor(mcpClient, null, // No LLM in check mode
|
|
518
|
+
discovery.tools, {
|
|
519
|
+
stepTimeout: workflowTimeout,
|
|
520
|
+
analyzeSteps: false, // No LLM analysis in check mode
|
|
521
|
+
generateSummary: false, // No LLM summary in check mode
|
|
522
|
+
continueOnError: false,
|
|
523
|
+
timeouts: workflowTimeouts,
|
|
524
|
+
});
|
|
525
|
+
for (const workflow of workflows) {
|
|
526
|
+
if (verbose) {
|
|
527
|
+
output.info(`Executing workflow: ${workflow.name}`);
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const workflowResult = await workflowExecutor.execute(workflow);
|
|
531
|
+
workflowResults.push(workflowResult);
|
|
532
|
+
const statusIcon = workflowResult.success ? '\u2713' : '\u2717';
|
|
533
|
+
const stepsInfo = `${workflowResult.steps.filter(s => s.success).length}/${workflow.steps.length} steps`;
|
|
534
|
+
if (workflowResult.success) {
|
|
535
|
+
output.success(` ${statusIcon} ${workflow.name} (${stepsInfo}) - ${workflowResult.durationMs}ms`);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
const failedStep = workflowResult.failedStepIndex !== undefined
|
|
539
|
+
? workflow.steps[workflowResult.failedStepIndex]
|
|
540
|
+
: undefined;
|
|
541
|
+
output.error(` ${statusIcon} ${workflow.name} (${stepsInfo}) - Failed at: ${failedStep?.tool ?? 'unknown'}`);
|
|
542
|
+
if (verbose && workflowResult.failureReason) {
|
|
543
|
+
output.info(` Reason: ${workflowResult.failureReason}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
output.error(` \u2717 ${workflow.name} - Error: ${error instanceof Error ? error.message : error}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Workflow summary
|
|
552
|
+
const passed = workflowResults.filter(r => r.success).length;
|
|
553
|
+
const failed = workflowResults.length - passed;
|
|
554
|
+
output.newline();
|
|
555
|
+
if (failed === 0) {
|
|
556
|
+
output.success(`Workflow testing complete: ${passed}/${workflowResults.length} passed`);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
output.warn(`Workflow testing complete: ${passed}/${workflowResults.length} passed, ${failed} failed`);
|
|
560
|
+
}
|
|
561
|
+
output.newline();
|
|
562
|
+
}
|
|
563
|
+
// Generate documentation (after security testing so findings can be included)
|
|
564
|
+
output.info('Generating documentation...');
|
|
565
|
+
const contractMd = generateContractMd(result, {
|
|
566
|
+
securityFingerprints: securityEnabled ? securityFingerprints : undefined,
|
|
567
|
+
workflowResults: workflowResults.length > 0 ? workflowResults : undefined,
|
|
568
|
+
exampleLength,
|
|
569
|
+
fullExamples,
|
|
570
|
+
maxExamplesPerTool,
|
|
571
|
+
targetConfidence,
|
|
572
|
+
countValidationAsSuccess: config.check.metrics.countValidationAsSuccess,
|
|
573
|
+
separateValidationMetrics: config.check.metrics.separateValidationMetrics,
|
|
574
|
+
});
|
|
575
|
+
const contractMdPath = join(docsDir, config.output.files.contractDoc);
|
|
576
|
+
writeFileSync(contractMdPath, contractMd);
|
|
577
|
+
output.info(`Written: ${contractMdPath}`);
|
|
578
|
+
// Always generate JSON report for check command
|
|
579
|
+
// Add workflow results to the result object for the JSON report
|
|
580
|
+
const resultWithWorkflows = workflowResults.length > 0
|
|
581
|
+
? { ...result, workflowResults }
|
|
582
|
+
: result;
|
|
583
|
+
let jsonReport;
|
|
584
|
+
try {
|
|
585
|
+
jsonReport = generateJsonReport(resultWithWorkflows, {
|
|
586
|
+
schemaUrl: REPORT_SCHEMAS.CHECK_REPORT_SCHEMA_URL,
|
|
587
|
+
validate: true,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
output.error(error instanceof Error ? error.message : String(error));
|
|
592
|
+
process.exit(EXIT_CODES.ERROR);
|
|
593
|
+
}
|
|
594
|
+
const jsonPath = join(outputDir, config.output.files.checkReport);
|
|
595
|
+
writeFileSync(jsonPath, jsonReport);
|
|
596
|
+
output.info(`Written: ${jsonPath}`);
|
|
597
|
+
// Create baseline from results
|
|
598
|
+
let currentBaseline = createBaseline(result, fullServerCommand);
|
|
599
|
+
// Attach security fingerprints to tool fingerprints if security testing was run
|
|
600
|
+
if (securityEnabled && securityFingerprints.size > 0) {
|
|
601
|
+
currentBaseline = {
|
|
602
|
+
...currentBaseline,
|
|
603
|
+
tools: currentBaseline.tools.map((tool) => {
|
|
604
|
+
const securityFp = securityFingerprints.get(tool.name);
|
|
605
|
+
if (securityFp) {
|
|
606
|
+
return { ...tool, securityFingerprint: securityFp };
|
|
607
|
+
}
|
|
608
|
+
return tool;
|
|
609
|
+
}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
// Merge cached fingerprints in incremental mode
|
|
613
|
+
if (incrementalResult && incrementalResult.cachedFingerprints.length > 0) {
|
|
614
|
+
// Merge new fingerprints with cached ones
|
|
615
|
+
const mergedTools = [
|
|
616
|
+
...currentBaseline.tools,
|
|
617
|
+
...incrementalResult.cachedFingerprints,
|
|
618
|
+
].sort((a, b) => a.name.localeCompare(b.name));
|
|
619
|
+
currentBaseline = {
|
|
620
|
+
...currentBaseline,
|
|
621
|
+
tools: mergedTools,
|
|
622
|
+
};
|
|
623
|
+
output.info(`Merged ${incrementalResult.cachedFingerprints.length} cached tool fingerprints`);
|
|
624
|
+
}
|
|
625
|
+
// Check statistical confidence of performance metrics
|
|
626
|
+
// Count tools that don't meet the target confidence level
|
|
627
|
+
const lowConfidenceTools = [];
|
|
628
|
+
const confidenceLevelOrder = ['low', 'medium', 'high'];
|
|
629
|
+
const targetIndex = confidenceLevelOrder.indexOf(targetConfidence);
|
|
630
|
+
for (const tool of currentBaseline.tools) {
|
|
631
|
+
// Use the actual computed confidence level (accounts for samples AND CV)
|
|
632
|
+
const actualConfidence = tool.performanceConfidence?.confidenceLevel ?? 'low';
|
|
633
|
+
const actualIndex = confidenceLevelOrder.indexOf(actualConfidence);
|
|
634
|
+
// Tool is "low confidence" if its actual confidence is below target
|
|
635
|
+
if (actualIndex < targetIndex) {
|
|
636
|
+
lowConfidenceTools.push(tool.name);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Report confidence status
|
|
640
|
+
if (lowConfidenceTools.length > 0) {
|
|
641
|
+
const totalTools = currentBaseline.tools.length;
|
|
642
|
+
const pct = Math.round((lowConfidenceTools.length / totalTools) * 100);
|
|
643
|
+
const confidenceLabel = colorizeConfidence(formatConfidenceLevel(targetConfidence), targetConfidence);
|
|
644
|
+
output.warn(`\n--- Confidence Warning ---`);
|
|
645
|
+
output.warn(`${lowConfidenceTools.length}/${totalTools} tool(s) (${pct}%) have low statistical confidence`);
|
|
646
|
+
output.warn(`Target confidence: ${confidenceLabel} (requires ${CHECK_SAMPLING.SAMPLES_FOR_CONFIDENCE[targetConfidence]}+ samples)`);
|
|
647
|
+
if (lowConfidenceTools.length <= 5) {
|
|
648
|
+
output.warn(`Affected tools: ${lowConfidenceTools.join(', ')}`);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
output.warn(`Affected tools: ${lowConfidenceTools.slice(0, 5).join(', ')} +${lowConfidenceTools.length - 5} more`);
|
|
652
|
+
}
|
|
653
|
+
output.info(`Tip: Run multiple times or increase check.sampling.minSamples for more stable metrics`);
|
|
654
|
+
// Exit with low confidence code if configured
|
|
655
|
+
if (failOnLowConfidence) {
|
|
656
|
+
output.error('\nFailing due to check.sampling.failOnLowConfidence: true');
|
|
657
|
+
process.exit(EXIT_CODES.LOW_CONFIDENCE);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
const confidenceLabel = colorizeConfidence(formatConfidenceLevel(targetConfidence), targetConfidence);
|
|
662
|
+
output.info(`\nConfidence: All tools meet ${confidenceLabel} confidence threshold`);
|
|
663
|
+
}
|
|
664
|
+
// Save baseline if configured
|
|
665
|
+
if (saveBaselinePath) {
|
|
666
|
+
writeFileSync(saveBaselinePath, JSON.stringify(currentBaseline, null, 2));
|
|
667
|
+
output.info(`\nBaseline saved: ${saveBaselinePath}`);
|
|
668
|
+
}
|
|
669
|
+
// Handle baseline comparison
|
|
670
|
+
if (baselinePath) {
|
|
671
|
+
if (!existsSync(baselinePath)) {
|
|
672
|
+
output.error(`\nBaseline file not found: ${baselinePath}`);
|
|
673
|
+
process.exit(EXIT_CODES.ERROR);
|
|
674
|
+
}
|
|
675
|
+
const previousBaseline = loadBaseline(baselinePath);
|
|
676
|
+
const rawDiff = compareBaselines(previousBaseline, currentBaseline, {
|
|
677
|
+
performanceThreshold, // Already resolved from config + CLI override
|
|
678
|
+
});
|
|
679
|
+
// Apply severity configuration (filtering, overrides)
|
|
680
|
+
const diff = applySeverityConfig(rawDiff, severityConfig);
|
|
681
|
+
output.info('\n--- Drift Report ---');
|
|
682
|
+
// Select formatter based on --format option
|
|
683
|
+
const formattedDiff = formatDiff(diff, diffFormat, baselinePath);
|
|
684
|
+
output.info(formattedDiff);
|
|
685
|
+
// Report performance regressions if detected
|
|
686
|
+
if (diff.performanceReport?.hasRegressions) {
|
|
687
|
+
output.warn('\n--- Performance Regressions ---');
|
|
688
|
+
for (const regression of diff.performanceReport.regressions) {
|
|
689
|
+
const percentStr = (regression.regressionPercent * 100).toFixed(1);
|
|
690
|
+
output.warn(` ${regression.toolName}: p50 ${regression.previousP50Ms.toFixed(0)}ms → ` +
|
|
691
|
+
`${regression.currentP50Ms.toFixed(0)}ms (+${percentStr}%)`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
else if (diff.performanceReport?.improvementCount ?? 0 > 0) {
|
|
695
|
+
output.info(`\nPerformance: ${diff.performanceReport?.improvementCount} tool(s) improved`);
|
|
696
|
+
}
|
|
697
|
+
// Report security changes if detected
|
|
698
|
+
if (diff.securityReport) {
|
|
699
|
+
const secReport = diff.securityReport;
|
|
700
|
+
if (secReport.newFindings.length > 0) {
|
|
701
|
+
output.error('\n--- New Security Findings ---');
|
|
702
|
+
for (const finding of secReport.newFindings) {
|
|
703
|
+
const icon = finding.riskLevel === 'critical' || finding.riskLevel === 'high'
|
|
704
|
+
? '!'
|
|
705
|
+
: finding.riskLevel === 'medium'
|
|
706
|
+
? '*'
|
|
707
|
+
: '-';
|
|
708
|
+
output.error(` ${icon} [${finding.riskLevel.toUpperCase()}] ${finding.tool}: ${finding.title}`);
|
|
709
|
+
output.info(` ${finding.cweId}: ${finding.description}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (secReport.resolvedFindings.length > 0) {
|
|
713
|
+
output.success(`\nSecurity: ${secReport.resolvedFindings.length} finding(s) resolved`);
|
|
714
|
+
}
|
|
715
|
+
if (secReport.degraded) {
|
|
716
|
+
output.error(`\nSecurity posture degraded: Risk score ${secReport.previousRiskScore} → ${secReport.currentRiskScore}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Handle --accept-drift flag
|
|
720
|
+
if (options.acceptDrift && diff.severity !== 'none') {
|
|
721
|
+
const acceptedBaseline = acceptDrift(currentBaseline, rawDiff, {
|
|
722
|
+
reason: options.acceptReason,
|
|
723
|
+
});
|
|
724
|
+
saveBaseline(acceptedBaseline, baselinePath);
|
|
725
|
+
output.success(`\nDrift accepted and baseline updated: ${baselinePath}`);
|
|
726
|
+
if (options.acceptReason) {
|
|
727
|
+
output.info(`Reason: ${options.acceptReason}`);
|
|
728
|
+
}
|
|
729
|
+
output.info('Future checks will compare against this new baseline.');
|
|
730
|
+
}
|
|
731
|
+
else if (!options.acceptDrift) {
|
|
732
|
+
// Check if diff meets failure threshold based on severity config
|
|
733
|
+
const shouldFail = shouldFailOnDiff(diff, severityConfig.failOnSeverity);
|
|
734
|
+
const exitCode = SEVERITY_TO_EXIT_CODE[diff.severity] ?? EXIT_CODES.CLEAN;
|
|
735
|
+
if (diff.severity === 'breaking') {
|
|
736
|
+
output.error('\nBreaking changes detected!');
|
|
737
|
+
output.error('Use --accept-drift to accept these changes as intentional.');
|
|
738
|
+
if (failOnDrift || shouldFail) {
|
|
739
|
+
process.exit(exitCode);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else if (diff.severity === 'warning') {
|
|
743
|
+
output.warn('\nWarning-level changes detected.');
|
|
744
|
+
output.warn('Use --accept-drift to accept these changes as intentional.');
|
|
745
|
+
if (failOnDrift || shouldFail) {
|
|
746
|
+
process.exit(exitCode);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else if (diff.severity === 'info') {
|
|
750
|
+
output.info('\nInfo-level changes detected (non-breaking).');
|
|
751
|
+
if (shouldFail) {
|
|
752
|
+
process.exit(exitCode);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Exit with appropriate code based on severity
|
|
756
|
+
// This provides semantic exit codes for CI/CD even when not failing
|
|
757
|
+
process.exit(exitCode);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (config.check.assertions.strict && (result.metadata.assertions?.failed ?? 0) > 0) {
|
|
761
|
+
output.error('\nAssertion failures detected and check.assertions.strict is enabled.');
|
|
762
|
+
process.exit(EXIT_CODES.ERROR);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
767
|
+
output.error('\n--- Check Failed ---');
|
|
768
|
+
output.error(`Error: ${errorMessage}`);
|
|
769
|
+
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('Connection refused')) {
|
|
770
|
+
output.error('\nPossible causes:');
|
|
771
|
+
output.error(' - The MCP server is not running');
|
|
772
|
+
output.error(' - The server address/port is incorrect');
|
|
773
|
+
}
|
|
774
|
+
else if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
|
|
775
|
+
output.error('\nPossible causes:');
|
|
776
|
+
output.error(' - The MCP server is taking too long to respond');
|
|
777
|
+
output.error(' - Increase server.timeout in bellwether.yaml');
|
|
778
|
+
}
|
|
779
|
+
else if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
|
|
780
|
+
output.error('\nPossible causes:');
|
|
781
|
+
output.error(' - The server command was not found');
|
|
782
|
+
output.error(' - Check that the command is installed and in PATH');
|
|
783
|
+
}
|
|
784
|
+
process.exit(EXIT_CODES.ERROR);
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
await mcpClient.disconnect();
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
/**
|
|
791
|
+
* Format a diff using the specified output format.
|
|
792
|
+
*
|
|
793
|
+
* @param diff - The behavioral diff to format
|
|
794
|
+
* @param format - Output format: text, json, compact, github, markdown, junit, sarif
|
|
795
|
+
* @param baselinePath - Path to baseline file (used for SARIF location references)
|
|
796
|
+
* @returns Formatted string
|
|
797
|
+
*/
|
|
798
|
+
function formatDiff(diff, format, baselinePath) {
|
|
799
|
+
switch (format.toLowerCase()) {
|
|
800
|
+
case 'json':
|
|
801
|
+
return formatDiffJson(diff);
|
|
802
|
+
case 'compact':
|
|
803
|
+
return formatDiffCompact(diff);
|
|
804
|
+
case 'github':
|
|
805
|
+
return formatDiffGitHubActions(diff);
|
|
806
|
+
case 'markdown':
|
|
807
|
+
case 'md':
|
|
808
|
+
return formatDiffMarkdown(diff);
|
|
809
|
+
case 'junit':
|
|
810
|
+
case 'junit-xml':
|
|
811
|
+
case 'xml':
|
|
812
|
+
return formatDiffJUnit(diff, 'bellwether-check');
|
|
813
|
+
case 'sarif':
|
|
814
|
+
return formatDiffSarif(diff, baselinePath);
|
|
815
|
+
case 'text':
|
|
816
|
+
default:
|
|
817
|
+
return formatDiffText(diff);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
//# sourceMappingURL=check.js.map
|