RAGOps
Evaluate a candidate locally, then inspect and review saved team runs from the same evidence contract.
Release decision
Failed gates
- Not evaluated
Set RAGOPS_STORE on the API to enable shared local history and review metadata.
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index e261f52..39161fa 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "ragops",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "Explainable regression tests and release gates for RAG systems and AI agents.",
"author": {
"name": "Thang Luu",
diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index ed6de0a..a65ceb9 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "ragops",
- "version": "1.0.0",
- "description": "Explainable regression tests and release gates for RAG systems and AI agents.",
+ "version": "2.0.0",
+ "description": "Offline, explainable release gates and evidence bundles for RAG systems and AI agents.",
"author": {
"name": "Thang Luu",
"url": "https://github.com/thangldw"
@@ -14,7 +14,7 @@
"interface": {
"displayName": "RAGOps",
"shortDescription": "Gate AI releases with evidence",
- "longDescription": "Compare candidate AI behavior with an accepted baseline, explain failed gates, and produce reviewable release evidence without requiring a model provider.",
+ "longDescription": "Compare recorded candidate behavior with an accepted baseline, localize regressions by policy and case, and produce verifiable release evidence without requiring a model provider.",
"developerName": "DUC THANG LUU",
"category": "Productivity",
"capabilities": ["Read"],
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7585af2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.11", "3.12", "3.13"]
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: pip
+ - name: Install test dependencies
+ run: python -m pip install --disable-pip-version-check -e '.[dev]' build
+ - name: Lint
+ run: ruff check .
+ - name: Test
+ env:
+ PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1"
+ run: pytest -q
+ - name: Build and inspect distributions
+ run: |
+ python -m build
+ python -m zipfile --list dist/ragops-*.whl | grep 'ragops/web/index.html'
+ python -m zipfile --list dist/ragops-*.whl | grep 'ragops/api/main.py'
+ - name: Run credential-free release gate
+ run: |
+ ragops demo --profile engineer --output demo-output
+ ragops evidence verify --bundle demo-output/evidence
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 4e20b3b..b1ed2c3 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -16,10 +16,10 @@ jobs:
contents: read
id-token: write
steps:
- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.tag }}
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Verify tag and package version
@@ -53,6 +53,7 @@ jobs:
--pattern 'ragops-*.whl' \
--pattern 'ragops-*.tar.gz' \
--pattern 'ragops-*.cdx.json' \
+ --pattern 'ragops-chatgpt-plugin-*.zip' \
--pattern 'SHA256SUMS' \
--dir release-assets
(cd release-assets && sha256sum --check SHA256SUMS)
diff --git a/.kimi-plugin/plugin.json b/.kimi-plugin/plugin.json
index 3c3108c..6430c6b 100644
--- a/.kimi-plugin/plugin.json
+++ b/.kimi-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "ragops",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "Explainable regression tests and release gates for RAG systems and AI agents.",
"author": {
"name": "Thang Luu",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 640e9ec..3f77b62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,28 @@
### English
-- Added an offline Phoenix recorded-score adapter example that preserves producer-owned score meaning, scale, and direction while enforcing complete case coverage.
+- No unreleased changes.
+
+## [2.0.0] - 2026-08-23
+
+### English
+
+- Added versioned contract migration, content-addressed evidence bundles, slice/distribution gates, evaluator calibration, trace-graph evaluation, dataset leakage controls, CI renderers, portable vendor adapters, and audited blind review.
+- Packaged the optional FastAPI workbench with CSP-safe DOM rendering and added Python 3.11-3.13 CI.
+- Upgraded the synthetic flagship demo to generate canonical JSON, Markdown, HTML, JUnit, SARIF, GitHub Summary, and verified evidence for executive, engineer, and auditor profiles.
+- Added a deterministic ChatGPT skills-only plugin bundle and v2 release evidence.
+
+### Tiếng Việt
+
+- Bổ sung contract có version, evidence bundle content-addressed, slice/distribution gate, calibration, trace graph, chống dataset leakage, CI renderer, vendor adapter và blind review có audit.
+- Đóng gói FastAPI workbench tùy chọn với CSP/DOM an toàn và CI Python 3.11-3.13.
+- Nâng demo synthetic để xuất cùng một quyết định qua JSON, Markdown, HTML, JUnit, SARIF, GitHub Summary và evidence đã xác minh.
+
+### 日本語
+
+- versioned contract、content-addressed evidence、slice/distribution gate、calibration、trace graph、dataset leakage check、CI renderer、vendor adapter、blind review audit を追加しました。
+- CSP-safe な任意 FastAPI workbench と Python 3.11-3.13 CI を同梱しました。
+- synthetic flagship demo は同一判定を JSON、Markdown、HTML、JUnit、SARIF、GitHub Summary、検証済み evidence に出力します。
## [1.2.0] - 2026-08-02
@@ -46,5 +67,6 @@
- リポジトリ所有の GitHub Actions をすべて削除し、リリースゲートはローカル CLI と API で引き続き利用できます。
- 公開タグとパッケージ版を `v1.0.0` に統一しました。
+[2.0.0]: https://github.com/thangldw/ragops/releases/tag/v2.0.0
[1.2.0]: https://github.com/thangldw/ragops/releases/tag/v1.2.0
[1.0.0]: https://github.com/thangldw/ragops/releases/tag/v1.0.0
diff --git a/README.md b/README.md
index b31560a..c98733f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[English](#english) · [Tiếng Việt](#tiếng-việt) · [日本語](#日本語)
-Regression tests and explainable release gates for RAG systems and AI agents.
+Offline, reproducible release gates for RAG systems and AI agents.
```mermaid
%%{init: {"theme":"base","themeVariables":{"background":"#FFFFFF","fontFamily":"Arial, sans-serif","lineColor":"#667085","primaryTextColor":"#172B4D"}}}%%
@@ -11,7 +11,7 @@ flowchart LR
B["Accepted baseline
Mốc chuẩn / 基準"]:::blue
C["Candidate traces
Ứng viên / 候補"]:::purple
E["Evaluators & policy
Đánh giá / 評価"]:::green
- G{"PASS / WARN / FAIL"}:::pink
+ G{"PASS / WARN / BLOCK"}:::pink
S --> E
B --> E
C --> E --> G
@@ -24,31 +24,34 @@ flowchart LR
## English
-RAGOps compares recorded candidate behavior with an accepted baseline, applies versioned release policy and returns explainable `PASS`, `WARN` or `FAIL` evidence. The dependency-free core works offline; optional adapters expose API and UI surfaces. Runtime integrations use the local CLI or API; the repository-owned GitHub Actions workflow is limited to PyPI publishing.
+RAGOps compares recorded candidate behavior with an accepted baseline, applies a versioned release policy, and emits one canonical `PASS`, `WARN`, or `BLOCK` decision across JSON, Markdown, HTML, JUnit, SARIF, and GitHub Summary. The dependency-free core performs no network calls. Optional adapters import vendor exports; the API/workbench ships in the wheel through the `api` extra.
Requirements: Python 3.11+.
-Current technical references: [architecture](docs/ARCHITECTURE.md), [operations](docs/OPERATIONS.md), [security](SECURITY.md) and [v1.2.0 release notes](docs/releases/v1.2.0.md).
+Current technical references: [architecture](docs/ARCHITECTURE.md), [contracts](docs/CONTRACTS.md), [governance](docs/governance.md), [operations](docs/OPERATIONS.md), [security](SECURITY.md), and [v2.0.0 release notes](docs/releases/v2.0.0.md).
The repository is also a skills-only plugin for ChatGPT, Codex, Claude Code and Cowork. It does not include a hosted MCP connector. See the [directory submission package](docs/submission/DIRECTORY_SUBMISSION.md), [privacy policy](PRIVACY.md), [terms](TERMS.md), and [support guidance](SUPPORT.md).
```bash
-python -m venv .venv
-.venv/bin/pip install -e '.[dev]'
-.venv/bin/ragops inspect --scenario scenarios/japanese_troubleshooting/benchmark-v0.2.json
-.venv/bin/ruff check .
-PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/pytest -q
+pip install ragops
+ragops demo --profile engineer --output ./demo-output
+ragops evidence verify --bundle ./demo-output/evidence
+open ./demo-output/release-report.html
```
+The credential-free demo intentionally returns `BLOCK`: citation coverage `1.0 -> 0.5`, citation precision `1.0 -> 0.5`, and lexical groundedness `1.0 -> 0.6`. It is synthetic benchmark evidence, not production adoption evidence.
+
+Install `ragops[api]` and run `ragops serve` for the local authenticated API/workbench. Use `ragops adapter list` for Phoenix, Ragas, DeepEval, LangSmith, MLflow, Promptfoo, custom JSON, and installed entry-point adapters.
+
## Tiếng Việt
-RAGOps so sánh hành vi ứng viên đã ghi lại với baseline được chấp nhận, áp dụng policy phát hành có version và trả về bằng chứng giải thích được ở trạng thái `PASS`, `WARN` hoặc `FAIL`. Core không có dependency và chạy offline; adapter tùy chọn cung cấp API/UI. Tích hợp runtime dùng CLI hoặc API cục bộ; GitHub Actions thuộc repo chỉ dùng để phát hành PyPI.
+RAGOps so sánh trace ứng viên với baseline được chấp nhận, áp dụng release policy có version và xuất cùng một quyết định `PASS`, `WARN` hoặc `BLOCK` sang JSON/Markdown/HTML/JUnit/SARIF. Core không dependency, chạy offline; API/workbench là extra tùy chọn. Demo synthetic cố ý tạo `BLOCK`, không phải bằng chứng production adoption.
Yêu cầu: Python 3.11+. Dùng các lệnh ở phần English để cài đặt, kiểm tra scenario, lint và test.
## 日本語
-RAGOps は、記録済みの候補動作を承認済みベースラインと比較し、バージョン管理されたリリースポリシーを適用して、説明可能な `PASS`・`WARN`・`FAIL` の根拠を返します。依存関係のないコアはオフラインで動作し、任意のアダプターが API と UI を提供します。runtime integration はローカル CLI/API を使い、repository-owned GitHub Actions は PyPI 公開のみに使用します。
+RAGOps は記録済み候補を承認済みベースラインと比較し、バージョン管理された方針から一つの `PASS`・`WARN`・`BLOCK` 判定を JSON、Markdown、HTML、JUnit、SARIF に出力します。依存関係のない core はオフラインで動作し、API/workbench は任意の extra です。デモは synthetic evidence であり、本番導入実績ではありません。
必要環境は Python 3.11 以上です。セットアップ、lint、テストには English セクションのコマンドを使用してください。
diff --git a/apps/api/main.py b/apps/api/main.py
index 514be41..b6103fc 100644
--- a/apps/api/main.py
+++ b/apps/api/main.py
@@ -1,378 +1,13 @@
-from __future__ import annotations
-
-import hmac
-import os
-from pathlib import Path
-
-from fastapi import Depends, FastAPI, Header, HTTPException, Request
-from fastapi.responses import FileResponse, JSONResponse
-from pydantic import BaseModel, ConfigDict
-
-from ragops import __version__
-from ragops.control_plane import ControlPlane
-from ragops.drift import detect_evaluator_drift
-from ragops.engine import compare, evaluate
-from ragops.loader import ContractError, responses_from_data, scenario_from_dict
-from ragops.models import (
- EvaluatorDriftMetricGate,
- EvaluatorDriftPolicy,
- SequentialPolicy,
- StatisticalMetricGate,
- StatisticalPolicy,
+# ruff: noqa: F401,F403
+"""Source-tree compatibility shim for the packaged API."""
+
+from ragops.api.main import * # noqa: F403
+from ragops.api.main import (
+ _positive_env_int,
+ _validate_collection_limits,
+ _validate_replay_limits,
+ evaluator_drift_endpoint,
+ require_api_key,
+ sequential_compare_endpoint,
+ statistical_compare_endpoint,
)
-from ragops.sequential import compare_replay_bundles_sequentially
-from ragops.statistical import compare_replay_bundles, replay_bundle_from_dict
-from ragops.store import ExperimentStore
-
-app = FastAPI(title="RAGOps API", version=__version__)
-
-DEFAULT_MAX_REQUEST_BYTES = 2 * 1024 * 1024
-DEFAULT_MAX_CASES = 1000
-DEFAULT_MAX_REPLAY_OBSERVATIONS = 100_000
-
-
-class EvaluateRequest(BaseModel):
- scenario: dict
- responses: list[dict]
-
-
-class SavedEvaluateRequest(EvaluateRequest):
- label: str = ""
-
-
-class CompareRequest(BaseModel):
- scenario: dict
- baseline: list[dict]
- candidate: list[dict]
-
-
-class StrictRequest(BaseModel):
- model_config = ConfigDict(extra="forbid")
-
-
-class StatisticalCompareRequest(StrictRequest):
- baseline: dict
- candidate: dict
- policy: dict
-
-
-class EvaluatorDriftRequest(StrictRequest):
- reference: dict
- current: dict
- policy: dict
-
-
-class ReviewRequest(BaseModel):
- status: str
- reviewer: str
- note: str = ""
-
-
-def configured_store(
- x_workspace_id: str | None = Header(default=None),
- x_workspace_key: str | None = Header(default=None),
-) -> ExperimentStore:
- control_root = os.getenv("RAGOPS_CONTROL_PLANE")
- if control_root:
- if not x_workspace_id or not x_workspace_key:
- raise HTTPException(status_code=401, detail="Workspace credentials are required")
- try:
- return ControlPlane(control_root).workspace_store(x_workspace_id, x_workspace_key)
- except PermissionError as exc:
- raise HTTPException(status_code=401, detail=str(exc)) from exc
- path = os.getenv("RAGOPS_STORE")
- if not path:
- raise HTTPException(status_code=503, detail="RAGOPS_STORE is not configured")
- return ExperimentStore(path)
-
-
-def require_api_key(x_api_key: str | None = Header(default=None)) -> None:
- expected = os.getenv("RAGOPS_API_KEY", "").strip()
- insecure_dev_mode = os.getenv("RAGOPS_INSECURE_DEV_MODE", "").casefold() == "true"
- if expected and insecure_dev_mode:
- raise HTTPException(
- status_code=503,
- detail="RAGOPS_API_KEY and RAGOPS_INSECURE_DEV_MODE cannot both be enabled",
- )
- if insecure_dev_mode:
- return
- if not expected:
- raise HTTPException(
- status_code=503,
- detail=(
- "API authentication is not configured; set RAGOPS_API_KEY or explicitly "
- "enable RAGOPS_INSECURE_DEV_MODE=true for local development"
- ),
- )
- if x_api_key is None or not hmac.compare_digest(x_api_key, expected):
- raise HTTPException(status_code=401, detail="Invalid API key")
-
-
-@app.middleware("http")
-async def enforce_request_size(request: Request, call_next):
- if request.method in {"POST", "PUT", "PATCH"}:
- maximum = _positive_env_int("RAGOPS_MAX_REQUEST_BYTES", DEFAULT_MAX_REQUEST_BYTES)
- content_length = request.headers.get("content-length")
- if content_length:
- try:
- declared = int(content_length)
- except ValueError:
- return JSONResponse(status_code=400, content={"detail": "Invalid Content-Length"})
- if declared > maximum:
- return JSONResponse(status_code=413, content={"detail": "Request body is too large"})
- body = await request.body()
- if len(body) > maximum:
- return JSONResponse(status_code=413, content={"detail": "Request body is too large"})
- return await call_next(request)
-
-
-def _positive_env_int(name: str, default: int) -> int:
- raw = os.getenv(name)
- if raw is None:
- return default
- try:
- value = int(raw)
- except ValueError as exc:
- raise HTTPException(status_code=503, detail=f"{name} must be a positive integer") from exc
- if value <= 0:
- raise HTTPException(status_code=503, detail=f"{name} must be a positive integer")
- return value
-
-
-def _validate_collection_limits(scenario: dict, *response_sets: list[dict]) -> None:
- maximum = _positive_env_int("RAGOPS_MAX_CASES", DEFAULT_MAX_CASES)
- cases = scenario.get("cases", [])
- if not isinstance(cases, list):
- return
- if len(cases) > maximum or any(len(responses) > maximum for responses in response_sets):
- raise HTTPException(
- status_code=413,
- detail=f"Scenario and response collections are limited to {maximum} cases",
- )
-
-
-def _validate_replay_limits(*bundles: dict) -> None:
- maximum_cases = _positive_env_int("RAGOPS_MAX_CASES", DEFAULT_MAX_CASES)
- maximum_observations = _positive_env_int(
- "RAGOPS_MAX_REPLAY_OBSERVATIONS", DEFAULT_MAX_REPLAY_OBSERVATIONS
- )
- for bundle in bundles:
- records = bundle.get("records", [])
- if not isinstance(records, list):
- continue
- case_ids = {
- record.get("case_id") for record in records if isinstance(record, dict)
- }
- if len(records) > maximum_observations or len(case_ids) > maximum_cases:
- raise HTTPException(
- status_code=413,
- detail=(
- f"Replay bundles are limited to {maximum_cases} cases and "
- f"{maximum_observations} observations"
- ),
- )
-
-
-@app.get("/health")
-def health() -> dict[str, str]:
- return {"status": "ok", "version": __version__}
-
-
-@app.get("/", include_in_schema=False)
-def dashboard() -> FileResponse:
- return FileResponse(Path(__file__).parents[1] / "web" / "index.html")
-
-
-@app.post("/v1/evaluate", dependencies=[Depends(require_api_key)])
-def evaluate_endpoint(payload: EvaluateRequest) -> dict:
- try:
- _validate_collection_limits(payload.scenario, payload.responses)
- scenario = scenario_from_dict(payload.scenario)
- responses = responses_from_data(payload.responses)
- return evaluate(scenario, responses).to_dict()
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.post("/v1/compare", dependencies=[Depends(require_api_key)])
-def compare_endpoint(payload: CompareRequest) -> dict:
- try:
- _validate_collection_limits(payload.scenario, payload.baseline, payload.candidate)
- scenario = scenario_from_dict(payload.scenario)
- baseline = responses_from_data(payload.baseline)
- candidate = responses_from_data(payload.candidate)
- return compare(scenario, baseline, candidate).to_dict()
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.post("/v1/statistical/compare", dependencies=[Depends(require_api_key)])
-def statistical_compare_endpoint(payload: StatisticalCompareRequest) -> dict:
- try:
- _validate_replay_limits(payload.baseline, payload.candidate)
- return compare_replay_bundles(
- replay_bundle_from_dict(payload.baseline),
- replay_bundle_from_dict(payload.candidate),
- _statistical_policy(payload.policy),
- ).to_dict()
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.post("/v1/statistical/sequential", dependencies=[Depends(require_api_key)])
-def sequential_compare_endpoint(payload: StatisticalCompareRequest) -> dict:
- try:
- _validate_replay_limits(payload.baseline, payload.candidate)
- return compare_replay_bundles_sequentially(
- replay_bundle_from_dict(payload.baseline),
- replay_bundle_from_dict(payload.candidate),
- _sequential_policy(payload.policy),
- ).to_dict()
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.post("/v1/evaluator-drift", dependencies=[Depends(require_api_key)])
-def evaluator_drift_endpoint(payload: EvaluatorDriftRequest) -> dict:
- try:
- _validate_replay_limits(payload.reference, payload.current)
- return detect_evaluator_drift(
- replay_bundle_from_dict(payload.reference),
- replay_bundle_from_dict(payload.current),
- _drift_policy(payload.policy),
- ).to_dict()
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.get("/v1/runs", dependencies=[Depends(require_api_key)])
-def list_runs(limit: int = 20, store: ExperimentStore = Depends(configured_store)) -> list[dict]:
- try:
- return store.list_runs(limit=limit)
- except ValueError as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.get("/v1/trends/{scenario_id}/{metric}", dependencies=[Depends(require_api_key)])
-def metric_trend(
- scenario_id: str,
- metric: str,
- limit: int = 50,
- store: ExperimentStore = Depends(configured_store),
-) -> list[dict]:
- try:
- return store.metric_trend(scenario_id, metric, limit=limit)
- except ValueError as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
-
-
-@app.post("/v1/runs/{run_id}/review", dependencies=[Depends(require_api_key)])
-def review_run(
- run_id: str,
- payload: ReviewRequest,
- store: ExperimentStore = Depends(configured_store),
-) -> dict[str, str]:
- try:
- store.review(
- run_id,
- status=payload.status,
- reviewer=payload.reviewer,
- note=payload.note,
- )
- except KeyError as exc:
- raise HTTPException(status_code=404, detail=str(exc)) from exc
- except ValueError as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
- return {"run_id": run_id, "review_status": payload.status}
-
-
-@app.post("/v1/runs/evaluate", dependencies=[Depends(require_api_key)])
-def evaluate_and_save(
- payload: SavedEvaluateRequest,
- store: ExperimentStore = Depends(configured_store),
-) -> dict:
- try:
- _validate_collection_limits(payload.scenario, payload.responses)
- scenario = scenario_from_dict(payload.scenario)
- responses = responses_from_data(payload.responses)
- report = evaluate(scenario, responses)
- except (ContractError, KeyError, TypeError, ValueError) as exc:
- raise HTTPException(status_code=422, detail=str(exc)) from exc
- run_id = store.save(report, label=payload.label)
- return {"run_id": run_id, "report": report.to_dict()}
-
-
-def _statistical_metric_gates(raw: object) -> dict[str, StatisticalMetricGate]:
- if not isinstance(raw, dict) or not raw:
- raise ContractError("Statistical API policy metrics must be a non-empty object")
- gates = {}
- for name, value in raw.items():
- if not isinstance(name, str) or not name or not isinstance(value, dict):
- raise ContractError("Statistical API metric gates must be named objects")
- if set(value) - {"direction", "minimum", "maximum", "max_regression"}:
- raise ContractError(f"Statistical API metric {name!r} has unknown fields")
- gates[name] = StatisticalMetricGate(
- direction=value["direction"],
- minimum=value.get("minimum"),
- maximum=value.get("maximum"),
- max_regression=value["max_regression"],
- )
- return gates
-
-
-def _statistical_policy(raw: dict) -> StatisticalPolicy:
- expected = {"confidence", "minimum_cases", "resamples", "seed", "metrics"}
- if set(raw) != expected:
- raise ContractError("Statistical API policy fields do not match the fixed policy contract")
- return StatisticalPolicy(
- confidence=raw["confidence"],
- minimum_cases=raw["minimum_cases"],
- resamples=raw["resamples"],
- seed=raw["seed"],
- metric_gates=_statistical_metric_gates(raw["metrics"]),
- )
-
-
-def _sequential_policy(raw: dict) -> SequentialPolicy:
- expected = {
- "confidence",
- "minimum_cases",
- "minimum_repeats",
- "maximum_repeats",
- "look_every",
- "resamples",
- "seed",
- "metrics",
- }
- if set(raw) != expected:
- raise ContractError("Statistical API policy fields do not match the sequential contract")
- return SequentialPolicy(
- confidence=raw["confidence"],
- minimum_cases=raw["minimum_cases"],
- minimum_repeats=raw["minimum_repeats"],
- maximum_repeats=raw["maximum_repeats"],
- look_every=raw["look_every"],
- resamples=raw["resamples"],
- seed=raw["seed"],
- metric_gates=_statistical_metric_gates(raw["metrics"]),
- )
-
-
-def _drift_policy(raw: dict) -> EvaluatorDriftPolicy:
- expected = {"confidence", "minimum_cases", "resamples", "seed", "metrics"}
- if set(raw) != expected or not isinstance(raw["metrics"], dict):
- raise ContractError("Evaluator drift API policy fields do not match the contract")
- gates = {}
- for name, value in raw["metrics"].items():
- if not isinstance(value, dict) or set(value) != {"max_absolute_change"}:
- raise ContractError(
- f"Evaluator drift API metric {name!r} must define max_absolute_change"
- )
- gates[name] = EvaluatorDriftMetricGate(value["max_absolute_change"])
- return EvaluatorDriftPolicy(
- confidence=raw["confidence"],
- minimum_cases=raw["minimum_cases"],
- resamples=raw["resamples"],
- seed=raw["seed"],
- metric_gates=gates,
- )
diff --git a/apps/web/index.html b/apps/web/index.html
index 0ef2ec8..8675a0a 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -3,10 +3,10 @@
Evaluate a candidate locally, then inspect and review saved team runs from the same evidence contract.
Set RAGOPS_STORE on the API to enable shared local history and review metadata.