evalgate-sdk 3.5.1__tar.gz → 3.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/CHANGELOG.md +5 -0
  2. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/PKG-INFO +1 -1
  3. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/pyproject.toml +1 -1
  4. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/__init__.py +10 -0
  5. evalgate_sdk-3.6.0/src/evalgate_sdk/_version.py +3 -0
  6. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/__init__.py +17 -0
  7. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/agent_evidence_utils.py +87 -0
  8. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/check_runner.py +291 -0
  9. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/cli_constants.py +5 -0
  10. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/commands.py +201 -157
  11. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/evidence_persistence.py +391 -0
  12. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/failure_modes.py +131 -0
  13. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/gate_evaluator.py +291 -0
  14. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/judge_commands.py +2 -2
  15. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/mcp.py +820 -0
  16. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/new_commands.py +118 -5
  17. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/regression_gate.py +91 -7
  18. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/run_cli.py +718 -0
  19. evalgate_sdk-3.6.0/src/evalgate_sdk/cli/sprint4_commands.py +38 -0
  20. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/client.py +34 -0
  21. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/types.py +52 -6
  22. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_cli_hardening_351.py +51 -15
  23. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_cli_new_commands.py +4 -0
  24. evalgate_sdk-3.6.0/tests/test_cli_sprint4.py +277 -0
  25. evalgate_sdk-3.6.0/tests/test_provider_keys.py +150 -0
  26. evalgate_sdk-3.5.1/src/evalgate_sdk/_version.py +0 -3
  27. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/.gitignore +0 -0
  28. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/README.md +0 -0
  29. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/assertions.py +0 -0
  30. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/auto.py +0 -0
  31. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/auto_experiment_runner.py +0 -0
  32. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/auto_failure_analyzer.py +0 -0
  33. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/auto_mutation_engine.py +0 -0
  34. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/batch.py +0 -0
  35. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cache.py +0 -0
  36. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/ci_context.py +0 -0
  37. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/api.py +0 -0
  38. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/api_command_config.py +0 -0
  39. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/config.py +0 -0
  40. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/env.py +0 -0
  41. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/formatters/types.py +0 -0
  42. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/golden_commands.py +0 -0
  43. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/manifest.py +0 -0
  44. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/policy_packs.py +0 -0
  45. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/profiles.py +0 -0
  46. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/render/__init__.py +0 -0
  47. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/render/snippet.py +0 -0
  48. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/render/sort.py +0 -0
  49. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/report/__init__.py +0 -0
  50. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/report/build_check_report.py +0 -0
  51. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/traces.py +0 -0
  52. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/workflow_commands.py +0 -0
  53. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cli/workspace.py +0 -0
  54. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/cluster.py +0 -0
  55. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/collector.py +0 -0
  56. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/constants.py +0 -0
  57. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/context.py +0 -0
  58. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/errors.py +0 -0
  59. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/export.py +0 -0
  60. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/formatters/__init__.py +0 -0
  61. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/formatters/github.py +0 -0
  62. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/formatters/human.py +0 -0
  63. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/formatters/json_fmt.py +0 -0
  64. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/formatters/pr_comment.py +0 -0
  65. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/golden.py +0 -0
  66. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/__init__.py +0 -0
  67. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/anthropic.py +0 -0
  68. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/autogen.py +0 -0
  69. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/crewai.py +0 -0
  70. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/langchain.py +0 -0
  71. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/openai.py +0 -0
  72. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/integrations/openai_eval.py +0 -0
  73. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/knowledge.py +0 -0
  74. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/local.py +0 -0
  75. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/logger.py +0 -0
  76. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/matchers.py +0 -0
  77. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/otel.py +0 -0
  78. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/pagination.py +0 -0
  79. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/py.typed +0 -0
  80. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/pytest_plugin.py +0 -0
  81. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/reason_codes.py +0 -0
  82. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/regression.py +0 -0
  83. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/replay_decision.py +0 -0
  84. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/__init__.py +0 -0
  85. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/adapters/__init__.py +0 -0
  86. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/adapters/config_to_dsl.py +0 -0
  87. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/adapters/testsuite_to_dsl.py +0 -0
  88. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/context.py +0 -0
  89. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/eval.py +0 -0
  90. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/execution_mode.py +0 -0
  91. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/executor.py +0 -0
  92. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/registry.py +0 -0
  93. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/run_report.py +0 -0
  94. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/runtime/types.py +0 -0
  95. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/snapshot.py +0 -0
  96. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/streaming.py +0 -0
  97. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/synthesize.py +0 -0
  98. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/testing.py +0 -0
  99. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/utils/__init__.py +0 -0
  100. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/utils/input_hash.py +0 -0
  101. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/src/evalgate_sdk/workflows.py +0 -0
  102. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/__init__.py +0 -0
  103. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_assertions.py +0 -0
  104. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_assertions_async.py +0 -0
  105. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_auto_mutation_engine_parity.py +0 -0
  106. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_auto_session_methods.py +0 -0
  107. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_batch_collector.py +0 -0
  108. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_ci_context.py +0 -0
  109. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_cli_golden_commands.py +0 -0
  110. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_client.py +0 -0
  111. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_collector.py +0 -0
  112. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_contract_payloads.py +0 -0
  113. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_decorators_and_integrations.py +0 -0
  114. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_errors.py +0 -0
  115. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_execution_mode.py +0 -0
  116. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_formatters.py +0 -0
  117. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_knowledge_methods.py +0 -0
  118. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_local_storage.py +0 -0
  119. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_new_modules.py +0 -0
  120. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_openapi_contracts.py +0 -0
  121. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_otel.py +0 -0
  122. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_parity_gap_modules.py +0 -0
  123. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_parity_gaps.py +0 -0
  124. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_prod_hardening.py +0 -0
  125. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_pytest_plugin.py +0 -0
  126. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_run_report.py +0 -0
  127. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_runtime_dsl.py +0 -0
  128. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_sdk_contract_scaffold.py +0 -0
  129. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_testing.py +0 -0
  130. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_types.py +0 -0
  131. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_workflows.py +0 -0
  132. {evalgate_sdk-3.5.1 → evalgate_sdk-3.6.0}/tests/test_workflows_parity.py +0 -0
@@ -9,6 +9,11 @@ Version numbering is aligned with the TypeScript SDK (`@evalgate/sdk`) and the p
9
9
 
10
10
  **Version history note:** The Python SDK jumped from 1.0.0 → 1.9.x → 2.0.0 to stay in sync with the TypeScript SDK. The TypeScript SDK had many releases (1.1–1.9) before the Python SDK existed. We now align both SDKs on the same major.minor version.
11
11
 
12
+ ## Unreleased
13
+
14
+ ### Added
15
+ - **Production-loop scaffold parity** — `evalgate init --template production-loop`, `evalgate verify --local`, configured `discover`, JSON `validate`, and JSON/dry-run `gate` now work together from a clean Python SDK install.
16
+
12
17
  ## [3.5.1] - 2026-06-26
13
18
 
14
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalgate-sdk
3
- Version: 3.5.1
3
+ Version: 3.6.0
4
4
  Summary: EvalGate Python SDK — CI for AI behavior. Traces, evaluations, assertions, and regression gates for LLM apps.
5
5
  Project-URL: Homepage, https://evalgate.com
6
6
  Project-URL: Documentation, https://github.com/evalgate/ai-evaluation-platform#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "evalgate-sdk"
7
- version = "3.5.1"
7
+ version = "3.6.0"
8
8
  description = "EvalGate Python SDK — CI for AI behavior. Traces, evaluations, assertions, and regression gates for LLM apps."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -392,12 +392,17 @@ from evalgate_sdk.types import (
392
392
  AutoPlanPreviewResponse,
393
393
  AutoSessionWorkflowProfile,
394
394
  CamelModel,
395
+ CreateProviderKeyParams,
395
396
  EvaluationIntegritySnapshot,
396
397
  EvaluationRunDetail,
397
398
  KnowledgeHintSummary,
398
399
  KnowledgeResponse,
400
+ ListProviderKeysParams,
401
+ ProviderKey,
399
402
  QualityBreakdown,
400
403
  QualityScore,
404
+ RotateProviderKeyResult,
405
+ UpdateProviderKeyParams,
401
406
  )
402
407
  from evalgate_sdk.utils.input_hash import normalize_input, sha256_input
403
408
  from evalgate_sdk.workflows import WorkflowTracer, create_workflow_tracer, trace_workflow_step
@@ -700,6 +705,11 @@ __all__ = [
700
705
  "CamelModel",
701
706
  "QualityScore",
702
707
  "QualityBreakdown",
708
+ "CreateProviderKeyParams",
709
+ "ListProviderKeysParams",
710
+ "ProviderKey",
711
+ "RotateProviderKeyResult",
712
+ "UpdateProviderKeyParams",
703
713
  # Knowledge API
704
714
  "KnowledgeAPI",
705
715
  # Collector (T2)
@@ -0,0 +1,3 @@
1
+ __version__ = "3.6.0"
2
+ SDK_VERSION = __version__
3
+ SPEC_VERSION = "3.6.0"
@@ -76,6 +76,7 @@ def get_app():
76
76
  share,
77
77
  sync,
78
78
  upgrade,
79
+ verify,
79
80
  version,
80
81
  )
81
82
  from evalgate_sdk.cli.judge_commands import judge
@@ -103,6 +104,7 @@ def get_app():
103
104
  app.command("sync")(sync)
104
105
  app.command("configure")(configure)
105
106
  app.command("upgrade")(upgrade)
107
+ app.command("verify")(verify)
106
108
  app.command("version")(version)
107
109
  app.command("impact-analysis")(impact_analysis)
108
110
  app.command("migrate")(migrate)
@@ -132,6 +134,21 @@ def get_app():
132
134
  app.command("replay-decision")(replay_decision)
133
135
  app.add_typer(auto_app, name="auto")
134
136
 
137
+ from evalgate_sdk.cli.sprint4_commands import failure_modes_command, mcp_command, run_cli_command
138
+
139
+ app.command(
140
+ "run-cli",
141
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
142
+ )(run_cli_command)
143
+ app.command(
144
+ "mcp",
145
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
146
+ )(mcp_command)
147
+ app.command(
148
+ "failure-modes",
149
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
150
+ )(failure_modes_command)
151
+
135
152
  return app
136
153
 
137
154
 
@@ -0,0 +1,87 @@
1
+ """Utilities for agent evidence capture (hashing, redaction, JSON I/O).
2
+
3
+ Port of ``cli/agent-evidence-utils.ts``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ SECRET_PATTERNS: list[re.Pattern[str]] = [
15
+ re.compile(
16
+ r"\b((?:api[_-]?key|access[_-]?token|auth[_-]?token|token|secret|password|client[_-]?secret)\s*[:=]\s*)([\"']?)([^\s\"',;]+)",
17
+ re.I,
18
+ ),
19
+ re.compile(r"(Bearer\s+)[A-Za-z0-9._~+/=-]+", re.I),
20
+ re.compile(r"\b(sk-[A-Za-z0-9_-]{20,})\b"),
21
+ re.compile(r"\b(gh[pousr]_[A-Za-z0-9_]{20,})\b"),
22
+ re.compile(r"\b(xox[baprs]-[A-Za-z0-9-]{20,})\b"),
23
+ re.compile(r"\b(npm_[A-Za-z0-9]{20,})\b"),
24
+ re.compile(r"\b(AKIA[0-9A-Z]{16})\b"),
25
+ re.compile(r"\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b"),
26
+ re.compile(r"(postgres(?:ql)?:\/\/[^:\s/@]+:)([^@\s]+)(@[^\s]+)", re.I),
27
+ ]
28
+
29
+
30
+ def sha256_hex(value: str) -> str:
31
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
32
+
33
+
34
+ def _sort_value(value: Any) -> Any:
35
+ if isinstance(value, list):
36
+ return [_sort_value(item) for item in value]
37
+ if isinstance(value, dict):
38
+ return {key: _sort_value(value[key]) for key in sorted(value)}
39
+ return value
40
+
41
+
42
+ def canonicalize(value: Any) -> str:
43
+ return json.dumps(_sort_value(value), separators=(",", ":"), default=str)
44
+
45
+
46
+ def stable_hash(value: Any) -> str:
47
+ return sha256_hex(canonicalize(value))
48
+
49
+
50
+ def redact_text(value: str) -> dict[str, Any]:
51
+ text = value
52
+ secrets_found = 0
53
+ for pattern in SECRET_PATTERNS:
54
+
55
+ def _replace(match: re.Match[str]) -> str:
56
+ nonlocal secrets_found
57
+ secrets_found += 1
58
+ groups = match.groups()
59
+ first = groups[0] if groups else ""
60
+ if first.lower().startswith("bearer"):
61
+ return f"{first}[REDACTED]"
62
+ if first.lower().startswith("postgres"):
63
+ suffix = groups[2] if len(groups) > 2 else ""
64
+ return f"{first}[REDACTED]{suffix}"
65
+ if len(groups) >= 3 and first:
66
+ quote = groups[1] if len(groups) > 1 else ""
67
+ return f"{first}{quote}[REDACTED]"
68
+ return "[REDACTED]"
69
+
70
+ text = pattern.sub(_replace, text)
71
+ return {"text": text, "secretsFound": secrets_found}
72
+
73
+
74
+ def bounded(value: str, max_bytes: int = 64 * 1024) -> str:
75
+ encoded = value.encode("utf-8")
76
+ if len(encoded) <= max_bytes:
77
+ return value
78
+ truncated = encoded[:max_bytes].decode("utf-8", errors="ignore")
79
+ return f"{truncated}\n[TRUNCATED]"
80
+
81
+
82
+ def read_json_file(file_path: str) -> dict[str, Any]:
83
+ raw = Path(file_path).read_text(encoding="utf-8-sig")
84
+ parsed = json.loads(raw)
85
+ if not isinstance(parsed, dict):
86
+ raise ValueError(f"{file_path} must contain a JSON object")
87
+ return parsed
@@ -0,0 +1,291 @@
1
+ """evalgate check runner — CI/CD evaluation gate.
2
+
3
+ Port of ``cli/check.ts`` (core path: fetch quality, evaluate gate, build report).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ from dataclasses import asdict, dataclass, is_dataclass
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ from evalgate_sdk.cli.api import fetch_quality_latest, fetch_run_details
15
+ from evalgate_sdk.cli.cli_constants import EXIT
16
+ from evalgate_sdk.cli.config import load_config, merge_config_with_args
17
+ from evalgate_sdk.cli.gate_evaluator import CheckGateArgs, evaluate_gate, gate_result_to_dict
18
+ from evalgate_sdk.cli.report.build_check_report import build_check_report
19
+ from evalgate_sdk.formatters.human import format_human
20
+ from evalgate_sdk.formatters.json_fmt import format_json
21
+
22
+ FormatType = Literal["human", "json", "github"]
23
+
24
+
25
+ @dataclass
26
+ class CheckArgs:
27
+ base_url: str
28
+ api_key: str
29
+ min_score: float
30
+ max_drop: float | None = None
31
+ warn_drop: float | None = None
32
+ min_n: int | None = None
33
+ allow_weak_evidence: bool = False
34
+ evaluation_id: str = ""
35
+ policy: str | None = None
36
+ baseline: str = "published"
37
+ format: FormatType = "human"
38
+ explain: bool = False
39
+ dry_run: bool = False
40
+ allow_empty: bool = False
41
+ max_cost_usd: float | None = None
42
+ max_latency_ms: float | None = None
43
+ max_cost_delta_usd: float | None = None
44
+
45
+
46
+ def _to_camel(key: str) -> str:
47
+ parts = key.split("_")
48
+ return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
49
+
50
+
51
+ def _serialize(value: Any) -> Any:
52
+ if is_dataclass(value) and not isinstance(value, type):
53
+ return {_to_camel(k): _serialize(v) for k, v in asdict(value).items() if v is not None}
54
+ if isinstance(value, list):
55
+ return [_serialize(item) for item in value]
56
+ if isinstance(value, dict):
57
+ return {_to_camel(k): _serialize(v) for k, v in value.items() if v is not None}
58
+ return value
59
+
60
+
61
+ def parse_check_args(argv: list[str]) -> tuple[CheckArgs | None, int, str | None]:
62
+ args: dict[str, str] = {}
63
+ index = 0
64
+ while index < len(argv):
65
+ arg = argv[index]
66
+ if arg.startswith("--"):
67
+ key = arg[2:]
68
+ next_value = argv[index + 1] if index + 1 < len(argv) else None
69
+ if next_value is not None and not next_value.startswith("--"):
70
+ args[key] = next_value
71
+ index += 2
72
+ else:
73
+ args[key] = "true"
74
+ index += 1
75
+ else:
76
+ index += 1
77
+
78
+ base_url = args.get("baseUrl") or args.get("base-url") or ""
79
+ api_key = args.get("apiKey") or args.get("api-key") or ""
80
+ min_score = int(args.get("minScore") or args.get("min-score") or "0")
81
+ max_drop = (
82
+ int(args["maxDrop"]) if args.get("maxDrop") else (int(args["max-drop"]) if args.get("max-drop") else None)
83
+ )
84
+ warn_drop = (
85
+ int(args["warnDrop"]) if args.get("warnDrop") else (int(args["warn-drop"]) if args.get("warn-drop") else None)
86
+ )
87
+ min_n = int(args["minN"]) if args.get("minN") else (int(args["min-n"]) if args.get("min-n") else None)
88
+ allow_weak_evidence = args.get("allowWeakEvidence") in {"true", "1"} or args.get("allow-weak-evidence") in {
89
+ "true",
90
+ "1",
91
+ }
92
+ evaluation_id = args.get("evaluationId") or args.get("evaluation-id") or ""
93
+ policy = args.get("policy")
94
+ format_raw = args.get("format") or "human"
95
+ fmt: FormatType = "json" if format_raw == "json" else ("github" if format_raw == "github" else "human")
96
+ explain = args.get("explain") in {"true", "1"}
97
+ dry_run = args.get("dry-run") == "true" or args.get("dryRun") == "true"
98
+ allow_empty = args.get("allow-empty") == "true" or args.get("allowEmpty") == "true"
99
+ profile = args.get("profile")
100
+ baseline = args.get("baseline") or "published"
101
+ if baseline not in {"published", "previous", "production", "auto"}:
102
+ baseline = "published"
103
+ max_cost_usd = (
104
+ float(args.get("max-cost-usd") or args.get("maxCostUsd") or "nan")
105
+ if (args.get("max-cost-usd") or args.get("maxCostUsd"))
106
+ else None
107
+ )
108
+ max_latency_ms = (
109
+ int(args.get("max-latency-ms") or args.get("maxLatencyMs") or "0")
110
+ if (args.get("max-latency-ms") or args.get("maxLatencyMs"))
111
+ else None
112
+ )
113
+ max_cost_delta_usd = (
114
+ float(args.get("max-cost-delta-usd") or args.get("maxCostDeltaUsd") or "nan")
115
+ if (args.get("max-cost-delta-usd") or args.get("maxCostDeltaUsd"))
116
+ else None
117
+ )
118
+
119
+ config = load_config()
120
+ merged = merge_config_with_args(
121
+ config,
122
+ {
123
+ "evaluation_id": evaluation_id or None,
124
+ "base_url": base_url or None,
125
+ "min_score": str(min_score) if args.get("minScore") or args.get("min-score") else None,
126
+ "max_drop": str(max_drop) if max_drop is not None else None,
127
+ "warn_drop": str(warn_drop) if warn_drop is not None else None,
128
+ "min_n": str(min_n) if min_n is not None else None,
129
+ "allow_weak_evidence": "true" if allow_weak_evidence else None,
130
+ "baseline": baseline,
131
+ "profile": profile,
132
+ },
133
+ )
134
+ if not evaluation_id and merged.get("evaluation_id"):
135
+ evaluation_id = str(merged["evaluation_id"])
136
+ if merged.get("base_url"):
137
+ base_url = str(merged["base_url"])
138
+ if merged.get("min_score") is not None and not (args.get("minScore") or args.get("min-score")):
139
+ min_score = int(merged["min_score"])
140
+ if merged.get("max_drop") is not None and max_drop is None:
141
+ max_drop = int(merged["max_drop"])
142
+ if merged.get("warn_drop") is not None and warn_drop is None:
143
+ warn_drop = int(merged["warn_drop"])
144
+ if merged.get("min_n") is not None and min_n is None:
145
+ min_n = int(merged["min_n"])
146
+ if merged.get("allow_weak_evidence") is not None and not (
147
+ args.get("allowWeakEvidence") or args.get("allow-weak-evidence")
148
+ ):
149
+ allow_weak_evidence = bool(merged["allow_weak_evidence"])
150
+ if merged.get("baseline") and not args.get("baseline"):
151
+ baseline = str(merged["baseline"])
152
+
153
+ import os
154
+
155
+ if not base_url:
156
+ from evalgate_sdk.constants import DEFAULT_BASE_URL
157
+
158
+ base_url = os.environ.get("EVALGATE_BASE_URL") or DEFAULT_BASE_URL
159
+ if not api_key:
160
+ api_key = os.environ.get("EVALGATE_API_KEY") or ""
161
+
162
+ if not api_key:
163
+ return None, EXIT.BAD_ARGS, "Error: --apiKey or EVALGATE_API_KEY is required"
164
+ if not evaluation_id:
165
+ return None, EXIT.BAD_ARGS, "Run evalgate init and paste your evaluationId, or pass --evaluationId."
166
+ if min_score < 0 or min_score > 100:
167
+ return None, EXIT.BAD_ARGS, "Error: --minScore must be 0-100"
168
+ if min_n is not None and min_n < 1:
169
+ return None, EXIT.BAD_ARGS, "Error: --minN must be a positive number"
170
+
171
+ return (
172
+ CheckArgs(
173
+ base_url=base_url,
174
+ api_key=api_key,
175
+ min_score=min_score,
176
+ max_drop=max_drop,
177
+ warn_drop=warn_drop,
178
+ min_n=min_n,
179
+ allow_weak_evidence=allow_weak_evidence,
180
+ evaluation_id=evaluation_id,
181
+ policy=policy,
182
+ baseline=baseline,
183
+ format=fmt,
184
+ explain=explain,
185
+ dry_run=dry_run,
186
+ allow_empty=allow_empty,
187
+ max_cost_usd=max_cost_usd,
188
+ max_latency_ms=max_latency_ms,
189
+ max_cost_delta_usd=max_cost_delta_usd,
190
+ ),
191
+ EXIT.PASS,
192
+ None,
193
+ )
194
+
195
+
196
+ async def run_check(args: CheckArgs) -> int:
197
+ quality_result = await fetch_quality_latest(args.base_url, args.api_key, args.evaluation_id, args.baseline)
198
+ if not quality_result.get("ok"):
199
+ status = quality_result.get("status", 0)
200
+ body = quality_result.get("body", "")
201
+ if status == 0:
202
+ print(f"EvalGate gate ERROR: Network failure — {body}")
203
+ else:
204
+ print(f"EvalGate gate ERROR: API returned {status} — {body}")
205
+ return EXIT.API_ERROR
206
+
207
+ quality = quality_result["data"]
208
+ request_id = quality_result.get("request_id")
209
+ evaluation_run_id = quality.get("evaluationRunId") or quality.get("evaluation_run_id")
210
+
211
+ run_details = None
212
+ if evaluation_run_id is not None:
213
+ run_res = await fetch_run_details(args.base_url, args.api_key, args.evaluation_id, int(evaluation_run_id))
214
+ if run_res.get("ok"):
215
+ run_details = run_res.get("data")
216
+
217
+ gate_args = CheckGateArgs(
218
+ min_score=args.min_score,
219
+ max_drop=args.max_drop,
220
+ warn_drop=args.warn_drop,
221
+ min_n=args.min_n,
222
+ allow_weak_evidence=args.allow_weak_evidence,
223
+ policy=args.policy,
224
+ baseline=args.baseline,
225
+ allow_empty=args.allow_empty,
226
+ max_cost_usd=args.max_cost_usd,
227
+ max_latency_ms=args.max_latency_ms,
228
+ max_cost_delta_usd=args.max_cost_delta_usd,
229
+ )
230
+ gate_result = evaluate_gate(gate_args, quality)
231
+ gate_dict = gate_result_to_dict(gate_result)
232
+
233
+ report = build_check_report(
234
+ evaluation_id=args.evaluation_id,
235
+ quality=quality,
236
+ gate_result=gate_dict,
237
+ base_url=args.base_url,
238
+ run_details=run_details,
239
+ request_id=request_id,
240
+ policy=args.policy,
241
+ min_score=args.min_score,
242
+ max_drop=args.max_drop,
243
+ warn_drop=args.warn_drop,
244
+ min_n=args.min_n,
245
+ allow_weak_evidence=args.allow_weak_evidence,
246
+ baseline=args.baseline,
247
+ max_cost_usd=args.max_cost_usd,
248
+ max_latency_ms=args.max_latency_ms,
249
+ max_cost_delta_usd=args.max_cost_delta_usd,
250
+ explain=args.explain,
251
+ )
252
+ report_dict = _serialize(report)
253
+
254
+ report_dir = Path.cwd() / ".evalgate"
255
+ try:
256
+ report_dir.mkdir(parents=True, exist_ok=True)
257
+ (report_dir / "last-report.json").write_text(json.dumps(report_dict, indent=2) + "\n", encoding="utf-8")
258
+ except OSError as exc:
259
+ print(f"EvalGate: failed to write .evalgate/last-report.json: {exc}")
260
+ return EXIT.API_ERROR
261
+
262
+ if args.format == "json":
263
+ print(format_json(report_dict))
264
+ elif args.format == "github":
265
+ from evalgate_sdk.formatters.github import format_github
266
+
267
+ print(format_github(report_dict))
268
+ else:
269
+ print(format_human(report_dict))
270
+
271
+ if not gate_result.passed:
272
+ print("\nNext: evalgate explain")
273
+
274
+ if args.dry_run:
275
+ print(f"\n[dry-run] Gate exits with code {gate_result.exit_code} (severity={gate_result.severity})")
276
+ return gate_result.exit_code
277
+
278
+ return gate_result.exit_code
279
+
280
+
281
+ def run_check_sync(argv: list[str]) -> int:
282
+ parsed, exit_code, message = parse_check_args(argv)
283
+ if parsed is None:
284
+ if message:
285
+ print(message)
286
+ return exit_code
287
+ try:
288
+ loop = asyncio.get_running_loop()
289
+ except RuntimeError:
290
+ return asyncio.run(run_check(parsed))
291
+ return loop.run_until_complete(run_check(parsed))
@@ -18,3 +18,8 @@ class EXIT:
18
18
  LOW_N = 6
19
19
  WEAK_EVIDENCE = 7
20
20
  WARN_REGRESSION = 8
21
+ FAILURE_MODE_THRESHOLD = 9
22
+ JUDGE_CREDIBILITY_UNTRUSTWORTHY = 10
23
+ IO_ERROR = 11
24
+ BASELINE_TAMPER = 12
25
+ INTERNAL_ERROR = 99