sable-sphynx 0.7.0.dev0__py3-none-any.whl
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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
sphynx/results.py
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
"""Immutable local grading, scorecards, and fair paired comparisons.
|
|
2
|
+
|
|
3
|
+
Only the installed illustrative RAG reranking pack is gradable in this release.
|
|
4
|
+
Candidate-visible captures contain no reference labels; this module loads the
|
|
5
|
+
separate public references after validating the saved runner evidence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Mapping
|
|
17
|
+
|
|
18
|
+
from .comparison_core import compare_validated_records
|
|
19
|
+
from .assessment_contracts import CaseView, EvidenceView, EvaluatorContext, _verified_evaluator_context
|
|
20
|
+
from .contracts import (
|
|
21
|
+
PACK_ID, CaseReferences, EvidenceGroup, ReferenceCandidate,
|
|
22
|
+
build_candidate_request, canonical_json,
|
|
23
|
+
)
|
|
24
|
+
from .evidence import (
|
|
25
|
+
RUN_SCHEMA_VERSION,
|
|
26
|
+
EvidenceError,
|
|
27
|
+
RunRef,
|
|
28
|
+
read_capture_artifact,
|
|
29
|
+
read_capture_manifest,
|
|
30
|
+
sha256_bytes,
|
|
31
|
+
verify_run,
|
|
32
|
+
write_json,
|
|
33
|
+
write_jsonl,
|
|
34
|
+
)
|
|
35
|
+
from .fast_search import FastSearchSmokeResult
|
|
36
|
+
from .metrics import (
|
|
37
|
+
GRADER_RELEASE,
|
|
38
|
+
METRIC_RELEASE,
|
|
39
|
+
CaseMetricResult,
|
|
40
|
+
MetricAggregate,
|
|
41
|
+
aggregate_case_metrics,
|
|
42
|
+
grade_case,
|
|
43
|
+
metric_definitions,
|
|
44
|
+
)
|
|
45
|
+
from .rag import load_rag_rerank_pack
|
|
46
|
+
from .records import EVALUATION_SCHEMA_VERSION
|
|
47
|
+
from .runner import ENV_POLICY_ID, MAX_STREAM_BYTES, _case_snapshot, _parse_response
|
|
48
|
+
|
|
49
|
+
METRICS_SCHEMA_VERSION = "sphynx.metrics.v1"
|
|
50
|
+
JUDGMENT_SCHEMA_VERSION = "sphynx.judgment.v1"
|
|
51
|
+
_EVALUATION_FILES = frozenset({"judgments.jsonl", "metrics.json"})
|
|
52
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
53
|
+
_RUN_ID = re.compile(r"run-[0-9a-f]{32}\Z")
|
|
54
|
+
_EVAL_ID = re.compile(r"eval-[0-9a-f]{32}\Z")
|
|
55
|
+
_GRADABLE = frozenset({"completed", "incomplete"})
|
|
56
|
+
_METRIC_IDS = tuple(definition.metric_id for definition in metric_definitions())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ResultError(ValueError):
|
|
60
|
+
"""An evaluation or comparison cannot be read or trusted."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _Ungradable(Exception):
|
|
64
|
+
def __init__(self, status: str, reason: str):
|
|
65
|
+
self.status = status
|
|
66
|
+
self.reason = reason
|
|
67
|
+
super().__init__(reason)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class EvaluationRef:
|
|
72
|
+
"""Reference to a published evaluation, or a wholly unreadable root."""
|
|
73
|
+
|
|
74
|
+
evaluation_id: str | None
|
|
75
|
+
path: Path | None
|
|
76
|
+
run_id: str | None
|
|
77
|
+
pack_id: str | None
|
|
78
|
+
status: str
|
|
79
|
+
reason: str | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class Scorecard:
|
|
84
|
+
data: dict[str, Any]
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> dict[str, Any]:
|
|
87
|
+
return json.loads(self.to_json())
|
|
88
|
+
|
|
89
|
+
def to_json(self) -> str:
|
|
90
|
+
return canonical_json(self.data)
|
|
91
|
+
|
|
92
|
+
def to_markdown(self) -> str:
|
|
93
|
+
data = self.data
|
|
94
|
+
counts = data["counts"]
|
|
95
|
+
releases = data["releases"]
|
|
96
|
+
lines = [
|
|
97
|
+
"# Sphynx illustrative RAG reranking scorecard",
|
|
98
|
+
"",
|
|
99
|
+
"**LOCAL_UNVERIFIED — synthetic/illustrative cases. This is not a validated benchmark or full-agent answer-quality result.**",
|
|
100
|
+
"",
|
|
101
|
+
f"- Evaluation: `{data['evaluation_id']}`; run: `{data['run_id']}`",
|
|
102
|
+
f"- Candidate: `{data['candidate']['name']}` version `{data['candidate']['version']}`",
|
|
103
|
+
f"- Pack: `{data['pack_id']}` release `{releases['pack_release']}`; track: `{data['track']}`",
|
|
104
|
+
f"- Cases: `{releases['dataset_id']}` version `{releases['dataset_version']}`; schema `{releases['case_schema_version']}`",
|
|
105
|
+
f"- Grader: `{releases['grader_release']}`; metrics: `{releases['metric_release']}`",
|
|
106
|
+
f"- Status: **{data['status']}**" + (f" — {data['reason']}" if data['reason'] else ""),
|
|
107
|
+
f"- Attempts: {counts['attempted_count'] if counts['attempted_count'] is not None else 'unverified'}; "
|
|
108
|
+
f"successful: {counts['success_count'] if counts['success_count'] is not None else 'unverified'}; "
|
|
109
|
+
f"failed: {counts['failure_count'] if counts['failure_count'] is not None else 'unverified'}",
|
|
110
|
+
f"- Primary quality coverage: {counts['observed_count']}/{counts['eligible_count']} eligible; "
|
|
111
|
+
f"missing {counts['missing_count']}; undefined {counts['undefined_count']}",
|
|
112
|
+
"",
|
|
113
|
+
"| Metric | Role | Value | Eligible | Observed | Missing | Undefined | Complete |",
|
|
114
|
+
"| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
|
|
115
|
+
]
|
|
116
|
+
for definition in data["metric_definitions"]:
|
|
117
|
+
metric = data["metrics"][definition["metric_id"]]
|
|
118
|
+
display_values = [(definition["metric_id"], metric["value"], "")]
|
|
119
|
+
if definition["metric_id"] == "runner_latency_ms":
|
|
120
|
+
display_values = [
|
|
121
|
+
("Runner latency p50", metric["value"], " ms"),
|
|
122
|
+
("Runner latency p95", metric["p95"], " ms"),
|
|
123
|
+
]
|
|
124
|
+
for label, number, unit in display_values:
|
|
125
|
+
value = "null" if number is None else f"{number:.6f}{unit}"
|
|
126
|
+
lines.append(
|
|
127
|
+
f"| {label} | {'primary' if definition['primary'] else 'diagnostic'} | "
|
|
128
|
+
f"{value} | {metric['eligible_count']} | {metric['observed_count']} | "
|
|
129
|
+
f"{metric['missing_count']} | {metric['undefined_count']} | {str(metric['complete']).lower()} |"
|
|
130
|
+
)
|
|
131
|
+
lines.extend(["", "A null quality value means the eligible cohort is incomplete; missing cases are never scored as zero."])
|
|
132
|
+
return "\n".join(lines) + "\n"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class Comparison:
|
|
137
|
+
data: dict[str, Any]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def compatible(self) -> bool:
|
|
141
|
+
return self.data["compatible"]
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def reason(self) -> str | None:
|
|
145
|
+
return self.data["reason"]
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> dict[str, Any]:
|
|
148
|
+
return json.loads(self.to_json())
|
|
149
|
+
|
|
150
|
+
def to_json(self) -> str:
|
|
151
|
+
return canonical_json(self.data)
|
|
152
|
+
|
|
153
|
+
def to_markdown(self) -> str:
|
|
154
|
+
data = self.data
|
|
155
|
+
lines = [
|
|
156
|
+
"# Sphynx illustrative paired RAG reranking comparison",
|
|
157
|
+
"",
|
|
158
|
+
"**LOCAL_UNVERIFIED — synthetic/illustrative cases. No full-agent or overall RAG claim.**",
|
|
159
|
+
"",
|
|
160
|
+
f"- Evaluation A: `{data['evaluation_a']}`",
|
|
161
|
+
f"- Evaluation B: `{data['evaluation_b']}`",
|
|
162
|
+
f"- Compatible: **{str(data['compatible']).lower()}**",
|
|
163
|
+
]
|
|
164
|
+
if not data["compatible"]:
|
|
165
|
+
lines.append(f"- Reason: {data['reason']}")
|
|
166
|
+
else:
|
|
167
|
+
lines.extend([
|
|
168
|
+
"",
|
|
169
|
+
"| Metric | Complete paired mean B − A | Observed pairs | Missing pairs | Undefined |",
|
|
170
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
171
|
+
])
|
|
172
|
+
for metric_id, item in data["metrics"].items():
|
|
173
|
+
value = "null" if item["mean_delta"] is None else f"{item['mean_delta']:.6f}"
|
|
174
|
+
lines.append(
|
|
175
|
+
f"| {metric_id} | {value} | {item['paired_count']} | "
|
|
176
|
+
f"{item['missing_pair_count']} | {item['undefined_count']} |"
|
|
177
|
+
)
|
|
178
|
+
lines.extend([
|
|
179
|
+
"",
|
|
180
|
+
"Primary nDCG@5 per-case pairs:",
|
|
181
|
+
"",
|
|
182
|
+
"| Case | A | B | B − A | Reason |",
|
|
183
|
+
"| --- | ---: | ---: | ---: | --- |",
|
|
184
|
+
])
|
|
185
|
+
for item in data["metrics"]["ndcg_at_5"]["per_case"]:
|
|
186
|
+
display = lambda value: "null" if value is None else f"{value:.6f}"
|
|
187
|
+
lines.append(
|
|
188
|
+
f"| {item['case_id']} | {display(item['value_a'])} | {display(item['value_b'])} | "
|
|
189
|
+
f"{display(item['delta_b_minus_a'])} | {item['reason'] or ''} |"
|
|
190
|
+
)
|
|
191
|
+
lines.extend(["", "JSON includes paired per-case values and missing reasons for every metric."])
|
|
192
|
+
return "\n".join(lines) + "\n"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _json_object(path: Path) -> dict[str, Any]:
|
|
196
|
+
try:
|
|
197
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
198
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
199
|
+
raise ResultError(f"cannot read JSON {path}: {exc}") from exc
|
|
200
|
+
if not isinstance(value, dict):
|
|
201
|
+
raise ResultError(f"JSON {path} must be an object")
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _expected_cases(pack: Any) -> list[dict[str, Any]]:
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
"dataset_id": case.dataset_id,
|
|
209
|
+
"dataset_version": case.dataset_version,
|
|
210
|
+
"case_id": case.case_id,
|
|
211
|
+
"query_id": case.query_id,
|
|
212
|
+
"pool_digest": case.pool_digest,
|
|
213
|
+
"split": case.split,
|
|
214
|
+
"split_group_id": case.split_group_id,
|
|
215
|
+
"cluster_id": case.cluster_id,
|
|
216
|
+
"suite_type": case.suite_type,
|
|
217
|
+
"versions": case.versions.to_dict(),
|
|
218
|
+
}
|
|
219
|
+
for case in pack.cases
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _expected_policy(pack: Any) -> dict[str, Any]:
|
|
224
|
+
policy = pack.descriptor.execution_policy
|
|
225
|
+
return {
|
|
226
|
+
"policy_version": policy.policy_version,
|
|
227
|
+
"trials": policy.trials,
|
|
228
|
+
"per_case_timeout_seconds": policy.per_case_timeout_seconds,
|
|
229
|
+
"resource_policy_id": policy.resource_policy_id,
|
|
230
|
+
"environment_policy_id": ENV_POLICY_ID,
|
|
231
|
+
"stdout_stderr_retention_limit_bytes_each": MAX_STREAM_BYTES,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _identity(manifest: Mapping[str, Any], pack: Any) -> dict[str, Any]:
|
|
236
|
+
return {
|
|
237
|
+
"pack_id": PACK_ID,
|
|
238
|
+
"pack_release": pack.descriptor.pack_release,
|
|
239
|
+
"track": pack.descriptor.track,
|
|
240
|
+
"cases": _expected_cases(pack),
|
|
241
|
+
"execution_policy": manifest.get("execution_policy") if isinstance(manifest.get("execution_policy"), dict) else _expected_policy(pack),
|
|
242
|
+
"runner": manifest.get("runner") if isinstance(manifest.get("runner"), dict) else {},
|
|
243
|
+
"trust_tier": "local_unverified",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _check_root(ref: RunRef, manifest: dict[str, Any], pack: Any) -> None:
|
|
248
|
+
if manifest.get("schema_version") != RUN_SCHEMA_VERSION:
|
|
249
|
+
raise _Ungradable("unsupported_schema", "unsupported run schema_version")
|
|
250
|
+
if manifest.get("pack_id") != PACK_ID or manifest.get("track") != pack.descriptor.track:
|
|
251
|
+
raise _Ungradable("incompatible", "run pack or track differs from installed RAG rerank pack")
|
|
252
|
+
if manifest.get("pack_release") != pack.descriptor.pack_release:
|
|
253
|
+
raise _Ungradable("incompatible", "run pack release differs from installed pack")
|
|
254
|
+
if manifest.get("cases") != _expected_cases(pack):
|
|
255
|
+
raise _Ungradable("incompatible", "run case order, pool, corpus, or cohort differs from installed pack")
|
|
256
|
+
policy = manifest.get("execution_policy")
|
|
257
|
+
if isinstance(policy, dict) and policy.get("trials") != 1:
|
|
258
|
+
raise _Ungradable("unsupported_schema", "only one pack-owned trial per case is supported")
|
|
259
|
+
if policy != _expected_policy(pack):
|
|
260
|
+
raise _Ungradable("incompatible", "run execution policy or resource/environment identity differs from installed pack")
|
|
261
|
+
if manifest.get("trust_tier") != "local_unverified" or manifest.get("provenance") != "runner_observed":
|
|
262
|
+
raise _Ungradable("incompatible", "run trust tier or provenance is unsupported")
|
|
263
|
+
candidate = manifest.get("candidate")
|
|
264
|
+
if not isinstance(candidate, dict) or set(candidate) != {"name", "version", "interface_version", "argv_sha256"}:
|
|
265
|
+
raise _Ungradable("incompatible", "run candidate identity is missing or malformed")
|
|
266
|
+
if any(not isinstance(candidate[key], str) or not candidate[key] for key in ("name", "version")):
|
|
267
|
+
raise _Ungradable("incompatible", "run candidate name or version is malformed")
|
|
268
|
+
if candidate.get("interface_version") != pack.descriptor.interface_version or not _DIGEST.fullmatch(str(candidate.get("argv_sha256"))):
|
|
269
|
+
raise _Ungradable("incompatible", "run candidate interface or argv identity is unsupported")
|
|
270
|
+
runner = manifest.get("runner")
|
|
271
|
+
if not isinstance(runner, dict) or set(runner) != {"release", "python_version", "platform", "clock_domain"}:
|
|
272
|
+
raise _Ungradable("incompatible", "run runner environment identity is missing")
|
|
273
|
+
if runner["release"] != "v1" or runner["clock_domain"] != "python.monotonic_ns.process_local":
|
|
274
|
+
raise _Ungradable("unsupported_schema", "run runner release or timing clock is unsupported")
|
|
275
|
+
if not runner["python_version"] or not runner["platform"]:
|
|
276
|
+
raise _Ungradable("incompatible", "run runner environment identity is empty")
|
|
277
|
+
try:
|
|
278
|
+
verify_run(ref)
|
|
279
|
+
except EvidenceError as exc:
|
|
280
|
+
raise _Ungradable("corrupt", f"run evidence failed integrity verification: {exc}") from exc
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _grade_rerank_context(context: EvaluatorContext, case: Any, pack: Any) -> CaseMetricResult:
|
|
284
|
+
"""Adapt a verified, grader-owned context to the unchanged native evaluator.
|
|
285
|
+
|
|
286
|
+
Domain schemas stay here. The common context neither validates RAG payloads
|
|
287
|
+
nor grants reference access. Native grade_case checks the exact case and
|
|
288
|
+
reference identities against the installed pack again.
|
|
289
|
+
"""
|
|
290
|
+
labels = context.references
|
|
291
|
+
references = CaseReferences(
|
|
292
|
+
case_id=labels["case_id"], label_authority=labels["label_authority"],
|
|
293
|
+
candidates=tuple(ReferenceCandidate(**dict(item)) for item in labels["candidates"]),
|
|
294
|
+
evidence_groups=tuple(EvidenceGroup(**dict(item)) for item in labels["evidence_groups"]),
|
|
295
|
+
)
|
|
296
|
+
return grade_case(
|
|
297
|
+
case, references, context.outputs["ranked_candidate_ids"],
|
|
298
|
+
context.outputs["runner_latency_ms"],
|
|
299
|
+
terminal_status=context.outputs["terminal_status"],
|
|
300
|
+
relevance_threshold=context.metadata["relevance_threshold"],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _capture_results(ref: RunRef, manifest: dict[str, Any], pack: Any) -> tuple[list[CaseMetricResult], list[dict[str, Any]], int]:
|
|
305
|
+
capture_ids = manifest["capture_ids"]
|
|
306
|
+
if len(capture_ids) != len(pack.cases):
|
|
307
|
+
raise EvidenceError("capture workload does not match one trial for every installed case")
|
|
308
|
+
results: list[CaseMetricResult] = []
|
|
309
|
+
judgments: list[dict[str, Any]] = []
|
|
310
|
+
successes = 0
|
|
311
|
+
for case, capture_id in zip(pack.cases, capture_ids):
|
|
312
|
+
capture = read_capture_manifest(ref, capture_id)
|
|
313
|
+
if capture.get("source") != "runner_observed":
|
|
314
|
+
raise EvidenceError(f"capture provenance is not runner observed: {capture_id}")
|
|
315
|
+
capture_identity = capture.get("identity")
|
|
316
|
+
if not isinstance(capture_identity, dict) or any((
|
|
317
|
+
capture_identity.get("run_id") != ref.run_id,
|
|
318
|
+
capture_identity.get("capture_id") != capture_id,
|
|
319
|
+
capture_identity.get("case_id") != case.case_id,
|
|
320
|
+
capture_identity.get("pool_digest") != case.pool_digest,
|
|
321
|
+
capture_identity.get("trial_index") != 1,
|
|
322
|
+
capture_identity.get("attempt_index") != 1,
|
|
323
|
+
capture_identity.get("trust_tier") != "local_unverified",
|
|
324
|
+
)):
|
|
325
|
+
raise EvidenceError(f"capture order, case, pool, or trial identity mismatch: {capture_id}")
|
|
326
|
+
request = build_candidate_request(case, PACK_ID)
|
|
327
|
+
expected_request = (request.to_json() + "\n").encode("utf-8")
|
|
328
|
+
expected_snapshot = (canonical_json(_case_snapshot(case)) + "\n").encode("utf-8")
|
|
329
|
+
if read_capture_artifact(ref, capture_id, "candidate_request") != expected_request:
|
|
330
|
+
raise EvidenceError(f"candidate request differs from installed case: {capture_id}")
|
|
331
|
+
if read_capture_artifact(ref, capture_id, "case_snapshot") != expected_snapshot:
|
|
332
|
+
raise EvidenceError(f"case snapshot differs from installed case: {capture_id}")
|
|
333
|
+
coverage = _json_object(ref.path / "captures" / capture_id / "coverage.json")
|
|
334
|
+
states = {item.get("capability_id"): item.get("state") for item in coverage.get("capabilities", []) if isinstance(item, dict)}
|
|
335
|
+
if any(states.get(item) != "available" for item in ("candidate_input", "case_snapshot", "runner_timing")):
|
|
336
|
+
raise EvidenceError(f"required case input or runner timing unavailable: {capture_id}")
|
|
337
|
+
timing = capture.get("timing")
|
|
338
|
+
latency_ns = timing.get("candidate_latency_ns") if isinstance(timing, dict) else None
|
|
339
|
+
if latency_ns is not None and (type(latency_ns) is not int or latency_ns < 0 or latency_ns > timing["elapsed_ns"]):
|
|
340
|
+
raise EvidenceError(f"candidate latency is invalid: {capture_id}")
|
|
341
|
+
latency_ms = latency_ns / 1_000_000 if latency_ns is not None else None
|
|
342
|
+
status = capture["status"]
|
|
343
|
+
ranked: tuple[str, ...] | None = None
|
|
344
|
+
if status == "success":
|
|
345
|
+
if states.get("candidate_stdout_raw") != "available" or states.get("candidate_response_validated") != "available":
|
|
346
|
+
raise EvidenceError(f"successful capture lacks validated raw stdout: {capture_id}")
|
|
347
|
+
stdout = read_capture_artifact(ref, capture_id, "candidate_stdout")
|
|
348
|
+
ranked, error_code, error_reason = _parse_response(stdout, request)
|
|
349
|
+
if error_code is not None or ranked is None:
|
|
350
|
+
raise EvidenceError(f"saved success response cannot be reconstructed: {capture_id}: {error_code}: {error_reason}")
|
|
351
|
+
if capture.get("ranked_candidate_ids") != list(ranked) or capture.get("failure_code") is not None:
|
|
352
|
+
raise EvidenceError(f"saved ranking or success status differs from raw stdout: {capture_id}")
|
|
353
|
+
if capture.get("exit_code") != 0 or latency_ms is None:
|
|
354
|
+
raise EvidenceError(f"saved success exit/timing is invalid: {capture_id}")
|
|
355
|
+
successes += 1
|
|
356
|
+
elif capture.get("ranked_candidate_ids") is not None or not isinstance(capture.get("failure_code"), str):
|
|
357
|
+
raise EvidenceError(f"failed capture has a ranking or lacks a failure code: {capture_id}")
|
|
358
|
+
elif status == "invalid_response":
|
|
359
|
+
# This outcome follows a completed, zero-exit process with a
|
|
360
|
+
# complete stdout capture. Re-derive its failure from those bytes;
|
|
361
|
+
# otherwise a rehashed manifest can hide a valid response.
|
|
362
|
+
if states.get("candidate_stdout_raw") != "available" or states.get("candidate_response_validated") != "not_captured":
|
|
363
|
+
raise EvidenceError(f"invalid response lacks complete raw stdout or has contradictory coverage: {capture_id}")
|
|
364
|
+
if capture.get("exit_code") != 0 or latency_ms is None:
|
|
365
|
+
raise EvidenceError(f"invalid response exit/timing is inconsistent: {capture_id}")
|
|
366
|
+
stdout = read_capture_artifact(ref, capture_id, "candidate_stdout")
|
|
367
|
+
parsed_ranking, error_code, _ = _parse_response(stdout, request)
|
|
368
|
+
if parsed_ranking is not None or error_code != capture["failure_code"]:
|
|
369
|
+
raise EvidenceError(f"invalid response status differs from raw stdout: {capture_id}")
|
|
370
|
+
try:
|
|
371
|
+
artifacts = [json.loads(line) for line in
|
|
372
|
+
(ref.path / "captures" / capture_id / "artifacts.jsonl").read_text(encoding="utf-8").splitlines()]
|
|
373
|
+
context = _verified_evaluator_context(
|
|
374
|
+
case=CaseView(case.case_id, request.to_dict()["schema_version"], request.to_dict()),
|
|
375
|
+
outputs={"ranked_candidate_ids": ranked, "runner_latency_ms": latency_ms,
|
|
376
|
+
"terminal_status": "ok" if status == "success" else status},
|
|
377
|
+
references=asdict(pack.reference_for(case.case_id)),
|
|
378
|
+
evidence=tuple(EvidenceView(
|
|
379
|
+
evidence_id=capture_id + ":" + artifact["role"],
|
|
380
|
+
sha256=artifact["sha256"], media_type=artifact["media_type"],
|
|
381
|
+
role=artifact["role"], source=artifact["source"], visibility="local_owner",
|
|
382
|
+
schema_version=artifact["schema_version"], case_id=case.case_id,
|
|
383
|
+
availability=("missing" if not artifact["observed"] else
|
|
384
|
+
"available" if artifact["complete"] else "truncated"),
|
|
385
|
+
metadata={"capture_id": capture_id, "retained_bytes": artifact["retained_bytes"],
|
|
386
|
+
"observed_bytes": artifact["observed_bytes"]},
|
|
387
|
+
) for artifact in artifacts),
|
|
388
|
+
run_id=ref.run_id, trust_tier=capture_identity["trust_tier"],
|
|
389
|
+
verified_by="sphynx.results._capture_results",
|
|
390
|
+
metadata={"relevance_threshold": pack.relevance_threshold,
|
|
391
|
+
"capture_id": capture_id, "trial": 1, "attempt": 1},
|
|
392
|
+
)
|
|
393
|
+
result = _grade_rerank_context(context, case, pack)
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
raise _Ungradable("grader_error", f"case grader failed for {case.case_id}: {type(exc).__name__}: {exc}") from exc
|
|
396
|
+
results.append(result)
|
|
397
|
+
judgments.append({
|
|
398
|
+
"schema_version": JUDGMENT_SCHEMA_VERSION,
|
|
399
|
+
"run_id": ref.run_id,
|
|
400
|
+
"capture_id": capture_id,
|
|
401
|
+
"case_id": case.case_id,
|
|
402
|
+
"pool_digest": case.pool_digest,
|
|
403
|
+
"trial_index": 1,
|
|
404
|
+
"status": "scored" if status == "success" else "candidate_failure",
|
|
405
|
+
"reason": None if status == "success" else capture.get("failure_reason") or capture["failure_code"],
|
|
406
|
+
"candidate_status": status,
|
|
407
|
+
"failure_code": capture.get("failure_code"),
|
|
408
|
+
"answerable": result.answerable,
|
|
409
|
+
"values": dict(result.values),
|
|
410
|
+
"undefined_reasons": dict(result.undefined_reasons),
|
|
411
|
+
})
|
|
412
|
+
return results, judgments, successes
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _verify_gradable_run(ref: RunRef) -> None:
|
|
416
|
+
"""Read-only semantic gate shared by grading and snapshot sync."""
|
|
417
|
+
|
|
418
|
+
manifest = _json_object(ref.path / "manifest.json")
|
|
419
|
+
pack = load_rag_rerank_pack()
|
|
420
|
+
_check_root(ref, manifest, pack)
|
|
421
|
+
_capture_results(ref, manifest, pack)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _non_gradable(pack: Any, status: str, reason: str, run_id: str) -> tuple[list[CaseMetricResult], list[dict[str, Any]]]:
|
|
425
|
+
results: list[CaseMetricResult] = []
|
|
426
|
+
judgments: list[dict[str, Any]] = []
|
|
427
|
+
for case in pack.cases:
|
|
428
|
+
answerable = any(group.required for group in pack.reference_for(case.case_id).evidence_groups)
|
|
429
|
+
values = tuple((metric_id, None) for metric_id in _METRIC_IDS)
|
|
430
|
+
reasons = tuple(
|
|
431
|
+
(metric_id, "runner_span_not_captured" if metric_id == "runner_latency_ms"
|
|
432
|
+
else f"candidate_{status}" if answerable else "unsupported_query")
|
|
433
|
+
for metric_id in _METRIC_IDS
|
|
434
|
+
)
|
|
435
|
+
result = CaseMetricResult(case.case_id, case.pool_digest, status, answerable, values, reasons)
|
|
436
|
+
results.append(result)
|
|
437
|
+
judgments.append({
|
|
438
|
+
"schema_version": JUDGMENT_SCHEMA_VERSION,
|
|
439
|
+
"run_id": run_id,
|
|
440
|
+
"capture_id": None,
|
|
441
|
+
"case_id": case.case_id,
|
|
442
|
+
"pool_digest": case.pool_digest,
|
|
443
|
+
"trial_index": 1,
|
|
444
|
+
"status": status,
|
|
445
|
+
"reason": reason,
|
|
446
|
+
"candidate_status": None,
|
|
447
|
+
"failure_code": None,
|
|
448
|
+
"answerable": result.answerable,
|
|
449
|
+
"values": dict(result.values),
|
|
450
|
+
"undefined_reasons": dict(result.undefined_reasons),
|
|
451
|
+
})
|
|
452
|
+
return results, judgments
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _publish(ref: RunRef, manifest: dict[str, Any], pack: Any, status: str, reason: str | None,
|
|
456
|
+
results: list[CaseMetricResult], judgments: list[dict[str, Any]], successes: int | None) -> EvaluationRef:
|
|
457
|
+
evaluation_id = "eval-" + uuid.uuid4().hex
|
|
458
|
+
for judgment in judgments:
|
|
459
|
+
judgment["evaluation_id"] = evaluation_id
|
|
460
|
+
try:
|
|
461
|
+
aggregates = aggregate_case_metrics(results)
|
|
462
|
+
except Exception:
|
|
463
|
+
if status != "grader_error":
|
|
464
|
+
raise
|
|
465
|
+
# A broken grading function must still leave an explicit null result.
|
|
466
|
+
# It is verified again before sync/report when the grader is available.
|
|
467
|
+
aggregates = {}
|
|
468
|
+
answerable_count = sum(any(group.required for group in pack.reference_for(case.case_id).evidence_groups)
|
|
469
|
+
for case in pack.cases)
|
|
470
|
+
for definition in metric_definitions():
|
|
471
|
+
eligible = len(pack.cases) if definition.metric_id == "runner_latency_ms" else answerable_count
|
|
472
|
+
aggregates[definition.metric_id] = MetricAggregate(
|
|
473
|
+
definition.metric_id, definition.release, definition.unit, definition.direction,
|
|
474
|
+
None, None, eligible, 0, eligible, len(pack.cases) - eligible, False,
|
|
475
|
+
)
|
|
476
|
+
primary = aggregates["ndcg_at_5"]
|
|
477
|
+
counts = {
|
|
478
|
+
"attempted_count": len(pack.cases) if successes is not None else None,
|
|
479
|
+
"success_count": successes,
|
|
480
|
+
"failure_count": len(pack.cases) - successes if successes is not None else None,
|
|
481
|
+
"eligible_count": primary.eligible_count,
|
|
482
|
+
"observed_count": primary.observed_count,
|
|
483
|
+
"missing_count": primary.missing_count,
|
|
484
|
+
"undefined_count": primary.undefined_count,
|
|
485
|
+
}
|
|
486
|
+
candidate = manifest.get("candidate") if isinstance(manifest.get("candidate"), dict) else {}
|
|
487
|
+
candidate = {
|
|
488
|
+
key: candidate.get(key) if isinstance(candidate.get(key), str) else "unknown"
|
|
489
|
+
for key in ("name", "version", "interface_version", "argv_sha256")
|
|
490
|
+
}
|
|
491
|
+
first = pack.cases[0]
|
|
492
|
+
releases = {
|
|
493
|
+
"pack_release": pack.descriptor.pack_release,
|
|
494
|
+
"case_schema_version": pack.descriptor.case_schema_version,
|
|
495
|
+
"dataset_id": first.dataset_id,
|
|
496
|
+
"dataset_version": first.dataset_version,
|
|
497
|
+
"dataset_sha256": pack.dataset_digest,
|
|
498
|
+
"grader_release": GRADER_RELEASE,
|
|
499
|
+
"metric_release": METRIC_RELEASE,
|
|
500
|
+
}
|
|
501
|
+
metric_data = {
|
|
502
|
+
"schema_version": METRICS_SCHEMA_VERSION,
|
|
503
|
+
"run_id": ref.run_id,
|
|
504
|
+
"evaluation_id": evaluation_id,
|
|
505
|
+
"pack_id": PACK_ID,
|
|
506
|
+
"track": pack.descriptor.track,
|
|
507
|
+
"status": status,
|
|
508
|
+
"reason": reason,
|
|
509
|
+
"trust_tier": "local_unverified",
|
|
510
|
+
"illustrative": True,
|
|
511
|
+
"candidate": candidate,
|
|
512
|
+
"releases": releases,
|
|
513
|
+
"identity": _identity(manifest, pack),
|
|
514
|
+
"counts": counts,
|
|
515
|
+
"metrics": {key: value.to_dict() for key, value in aggregates.items()},
|
|
516
|
+
"metric_definitions": [definition.to_dict() for definition in metric_definitions()],
|
|
517
|
+
}
|
|
518
|
+
stage = ref.path / (".staging-" + evaluation_id)
|
|
519
|
+
published = ref.path / "evaluations" / evaluation_id
|
|
520
|
+
stage.mkdir(parents=False, exist_ok=False)
|
|
521
|
+
try:
|
|
522
|
+
judgments_hash = write_jsonl(stage / "judgments.jsonl", judgments)
|
|
523
|
+
metrics_hash = write_json(stage / "metrics.json", metric_data)
|
|
524
|
+
files = {
|
|
525
|
+
"judgments.jsonl": {"sha256": judgments_hash, "size_bytes": (stage / "judgments.jsonl").stat().st_size},
|
|
526
|
+
"metrics.json": {"sha256": metrics_hash, "size_bytes": (stage / "metrics.json").stat().st_size},
|
|
527
|
+
}
|
|
528
|
+
evaluation_manifest = {
|
|
529
|
+
"schema_version": EVALUATION_SCHEMA_VERSION,
|
|
530
|
+
"run_id": ref.run_id,
|
|
531
|
+
"evaluation_id": evaluation_id,
|
|
532
|
+
"pack_id": PACK_ID,
|
|
533
|
+
"status": status,
|
|
534
|
+
"reason": reason,
|
|
535
|
+
"pack_release": pack.descriptor.pack_release,
|
|
536
|
+
"grader_release": GRADER_RELEASE,
|
|
537
|
+
"metric_release": METRIC_RELEASE,
|
|
538
|
+
"trust_tier": "local_unverified",
|
|
539
|
+
"files": files,
|
|
540
|
+
}
|
|
541
|
+
write_json(stage / "manifest.json", evaluation_manifest)
|
|
542
|
+
published.parent.mkdir(exist_ok=True)
|
|
543
|
+
os.replace(stage, published)
|
|
544
|
+
finally:
|
|
545
|
+
if stage.exists():
|
|
546
|
+
for child in stage.iterdir():
|
|
547
|
+
if child.is_file():
|
|
548
|
+
child.unlink()
|
|
549
|
+
stage.rmdir()
|
|
550
|
+
return EvaluationRef(evaluation_id, published.resolve(), ref.run_id, PACK_ID, status, reason)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def grade(run_ref: RunRef | str | Path) -> EvaluationRef:
|
|
554
|
+
"""Regrade saved captures without starting or changing a candidate process."""
|
|
555
|
+
|
|
556
|
+
run_path = (run_ref.path if isinstance(run_ref, RunRef) else Path(run_ref)).expanduser().resolve()
|
|
557
|
+
try:
|
|
558
|
+
manifest = _json_object(run_path / "manifest.json")
|
|
559
|
+
except ResultError as exc:
|
|
560
|
+
return EvaluationRef(None, None, None, None, "corrupt", str(exc))
|
|
561
|
+
run_id = manifest.get("run_id")
|
|
562
|
+
if not isinstance(run_id, str) or not _RUN_ID.fullmatch(run_id) or run_path.name != run_id:
|
|
563
|
+
return EvaluationRef(None, None, None, None, "corrupt", "run identity is unreadable or differs from directory")
|
|
564
|
+
ref = RunRef(run_id, run_path, manifest.get("pack_id") if isinstance(manifest.get("pack_id"), str) else "unknown")
|
|
565
|
+
try:
|
|
566
|
+
pack = load_rag_rerank_pack()
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
return EvaluationRef(None, None, run_id, ref.pack_id, "grader_error", f"installed grader/pack could not load: {exc}")
|
|
569
|
+
status, reason, successes = "completed", None, None
|
|
570
|
+
try:
|
|
571
|
+
_check_root(ref, manifest, pack)
|
|
572
|
+
results, judgments, successes = _capture_results(ref, manifest, pack)
|
|
573
|
+
if successes != len(pack.cases):
|
|
574
|
+
status = "incomplete"
|
|
575
|
+
reason = f"{len(pack.cases) - successes} candidate attempt(s) failed"
|
|
576
|
+
except _Ungradable as exc:
|
|
577
|
+
status, reason = exc.status, exc.reason
|
|
578
|
+
results, judgments = _non_gradable(pack, status, reason, run_id)
|
|
579
|
+
except (EvidenceError, ResultError, KeyError, TypeError, ValueError) as exc:
|
|
580
|
+
status, reason = "corrupt", f"capture evidence failed validation: {exc}"
|
|
581
|
+
results, judgments = _non_gradable(pack, status, reason, run_id)
|
|
582
|
+
except Exception as exc:
|
|
583
|
+
status, reason = "grader_error", f"grader failed: {type(exc).__name__}: {exc}"
|
|
584
|
+
results, judgments = _non_gradable(pack, status, reason, run_id)
|
|
585
|
+
if status not in _GRADABLE:
|
|
586
|
+
successes = None
|
|
587
|
+
try:
|
|
588
|
+
aggregate_case_metrics(results)
|
|
589
|
+
except Exception as exc:
|
|
590
|
+
status, reason, successes = "grader_error", f"grader aggregation failed: {type(exc).__name__}: {exc}", None
|
|
591
|
+
results, judgments = _non_gradable(pack, status, reason, run_id)
|
|
592
|
+
return _publish(ref, manifest, pack, status, reason, results, judgments, successes)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def open_evaluation(value: EvaluationRef | str | Path) -> EvaluationRef:
|
|
596
|
+
"""Open a published evaluation path without reading candidate code."""
|
|
597
|
+
|
|
598
|
+
if isinstance(value, EvaluationRef) and value.path is None:
|
|
599
|
+
return value
|
|
600
|
+
path = (value.path if isinstance(value, EvaluationRef) else Path(value)).expanduser().resolve()
|
|
601
|
+
manifest = _json_object(path / "manifest.json")
|
|
602
|
+
evaluation_id = manifest.get("evaluation_id")
|
|
603
|
+
run_id = manifest.get("run_id")
|
|
604
|
+
if not isinstance(evaluation_id, str) or not _EVAL_ID.fullmatch(evaluation_id) or path.name != evaluation_id:
|
|
605
|
+
raise ResultError("evaluation ID differs from directory")
|
|
606
|
+
if not isinstance(run_id, str) or not _RUN_ID.fullmatch(run_id) or path.parent.name != "evaluations" or path.parent.parent.name != run_id:
|
|
607
|
+
raise ResultError("evaluation run identity differs from directory")
|
|
608
|
+
return EvaluationRef(evaluation_id, path, run_id, manifest.get("pack_id"), manifest.get("status"), manifest.get("reason"))
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def verify_evaluation(value: EvaluationRef | str | Path) -> EvaluationRef:
|
|
612
|
+
"""Validate a complete published evaluation and its exact hashed inventory."""
|
|
613
|
+
|
|
614
|
+
ref = open_evaluation(value)
|
|
615
|
+
if ref.path is None:
|
|
616
|
+
raise ResultError(ref.reason or "evaluation was not durably published")
|
|
617
|
+
manifest = _json_object(ref.path / "manifest.json")
|
|
618
|
+
if set(manifest) != {"schema_version", "run_id", "evaluation_id", "pack_id", "status", "reason",
|
|
619
|
+
"pack_release", "grader_release", "metric_release", "trust_tier", "files"}:
|
|
620
|
+
raise ResultError("evaluation manifest fields are incomplete or unsupported")
|
|
621
|
+
if manifest.get("schema_version") != EVALUATION_SCHEMA_VERSION:
|
|
622
|
+
raise ResultError("unsupported evaluation schema_version")
|
|
623
|
+
if manifest.get("pack_id") != PACK_ID or manifest.get("trust_tier") != "local_unverified":
|
|
624
|
+
raise ResultError("evaluation pack or trust tier is unsupported")
|
|
625
|
+
if manifest.get("grader_release") != GRADER_RELEASE or manifest.get("metric_release") != METRIC_RELEASE:
|
|
626
|
+
raise ResultError("evaluation grader or metric release is unsupported")
|
|
627
|
+
if manifest.get("status") not in _GRADABLE | {"corrupt", "incompatible", "unsupported_schema", "grader_error"}:
|
|
628
|
+
raise ResultError("evaluation status is unsupported")
|
|
629
|
+
if manifest.get("status") == "completed" and manifest.get("reason") is not None:
|
|
630
|
+
raise ResultError("completed evaluation cannot have a failure reason")
|
|
631
|
+
if manifest.get("status") != "completed" and not isinstance(manifest.get("reason"), str):
|
|
632
|
+
raise ResultError("non-complete evaluation needs a reason")
|
|
633
|
+
files = manifest.get("files")
|
|
634
|
+
if not isinstance(files, dict) or set(files) != _EVALUATION_FILES:
|
|
635
|
+
raise ResultError("evaluation file inventory must contain exactly judgments.jsonl and metrics.json")
|
|
636
|
+
if {item.name for item in ref.path.iterdir()} != _EVALUATION_FILES | {"manifest.json"}:
|
|
637
|
+
raise ResultError("evaluation directory has missing or unindexed files")
|
|
638
|
+
for filename in _EVALUATION_FILES:
|
|
639
|
+
item = files[filename]
|
|
640
|
+
if not isinstance(item, dict) or set(item) != {"sha256", "size_bytes"}:
|
|
641
|
+
raise ResultError(f"invalid file inventory entry: {filename}")
|
|
642
|
+
if not isinstance(item["sha256"], str) or not _DIGEST.fullmatch(item["sha256"]):
|
|
643
|
+
raise ResultError(f"invalid file digest: {filename}")
|
|
644
|
+
if type(item["size_bytes"]) is not int or item["size_bytes"] < 0:
|
|
645
|
+
raise ResultError(f"invalid file size: {filename}")
|
|
646
|
+
try:
|
|
647
|
+
raw = (ref.path / filename).read_bytes()
|
|
648
|
+
except OSError as exc:
|
|
649
|
+
raise ResultError(f"cannot read evaluation file {filename}: {exc}") from exc
|
|
650
|
+
if sha256_bytes(raw) != item["sha256"] or len(raw) != item["size_bytes"]:
|
|
651
|
+
raise ResultError(f"evaluation file hash/size mismatch: {filename}")
|
|
652
|
+
metrics = _json_object(ref.path / "metrics.json")
|
|
653
|
+
releases = metrics.get("releases")
|
|
654
|
+
identity = metrics.get("identity")
|
|
655
|
+
candidate = metrics.get("candidate")
|
|
656
|
+
counts = metrics.get("counts")
|
|
657
|
+
definitions = metrics.get("metric_definitions")
|
|
658
|
+
if any(not isinstance(item, dict) for item in (releases, identity, candidate, counts)):
|
|
659
|
+
raise ResultError("metrics release, identity, candidate, or count object is malformed")
|
|
660
|
+
if not isinstance(definitions, list) or definitions != [definition.to_dict() for definition in metric_definitions()]:
|
|
661
|
+
raise ResultError("metric definitions differ from installed release")
|
|
662
|
+
if set(candidate) != {"name", "version", "interface_version", "argv_sha256"} or any(
|
|
663
|
+
not isinstance(item, str) for item in candidate.values()
|
|
664
|
+
):
|
|
665
|
+
raise ResultError("evaluation candidate identity is malformed")
|
|
666
|
+
if set(releases) != {"pack_release", "case_schema_version", "dataset_id", "dataset_version",
|
|
667
|
+
"dataset_sha256", "grader_release", "metric_release"}:
|
|
668
|
+
raise ResultError("evaluation release identity is malformed")
|
|
669
|
+
pack = load_rag_rerank_pack()
|
|
670
|
+
if releases != {
|
|
671
|
+
"pack_release": pack.descriptor.pack_release,
|
|
672
|
+
"case_schema_version": pack.descriptor.case_schema_version,
|
|
673
|
+
"dataset_id": pack.cases[0].dataset_id,
|
|
674
|
+
"dataset_version": pack.cases[0].dataset_version,
|
|
675
|
+
"dataset_sha256": pack.dataset_digest,
|
|
676
|
+
"grader_release": GRADER_RELEASE,
|
|
677
|
+
"metric_release": METRIC_RELEASE,
|
|
678
|
+
}:
|
|
679
|
+
raise ResultError("evaluation release differs from installed pack/metric release")
|
|
680
|
+
if set(identity) != {"pack_id", "pack_release", "track", "cases", "execution_policy", "runner", "trust_tier"}:
|
|
681
|
+
raise ResultError("evaluation comparison identity is malformed")
|
|
682
|
+
if (identity["pack_id"] != PACK_ID or identity["pack_release"] != pack.descriptor.pack_release
|
|
683
|
+
or identity["track"] != pack.descriptor.track or identity["cases"] != _expected_cases(pack)
|
|
684
|
+
or identity["trust_tier"] != "local_unverified"):
|
|
685
|
+
raise ResultError("evaluation comparison identity differs from installed pack")
|
|
686
|
+
if not isinstance(identity["execution_policy"], dict) or not isinstance(identity["runner"], dict):
|
|
687
|
+
raise ResultError("evaluation execution/runner identity is malformed")
|
|
688
|
+
if metrics.get("track") != pack.descriptor.track or manifest.get("pack_release") != pack.descriptor.pack_release:
|
|
689
|
+
raise ResultError("evaluation track or pack release differs from manifest")
|
|
690
|
+
if any((
|
|
691
|
+
metrics.get("schema_version") != METRICS_SCHEMA_VERSION,
|
|
692
|
+
metrics.get("run_id") != ref.run_id,
|
|
693
|
+
metrics.get("evaluation_id") != ref.evaluation_id,
|
|
694
|
+
metrics.get("pack_id") != PACK_ID,
|
|
695
|
+
metrics.get("status") != ref.status,
|
|
696
|
+
metrics.get("reason") != ref.reason,
|
|
697
|
+
metrics.get("trust_tier") != "local_unverified",
|
|
698
|
+
metrics.get("illustrative") is not True,
|
|
699
|
+
releases.get("grader_release") != GRADER_RELEASE,
|
|
700
|
+
releases.get("metric_release") != METRIC_RELEASE,
|
|
701
|
+
)):
|
|
702
|
+
raise ResultError("metrics identity, release, or status differs from evaluation manifest")
|
|
703
|
+
if not isinstance(metrics.get("metrics"), dict) or set(metrics["metrics"]) != set(_METRIC_IDS):
|
|
704
|
+
raise ResultError("evaluation metric set is incomplete")
|
|
705
|
+
try:
|
|
706
|
+
judgments = [json.loads(line) for line in (ref.path / "judgments.jsonl").read_text(encoding="utf-8").splitlines()]
|
|
707
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
708
|
+
raise ResultError(f"cannot read evaluation judgments: {exc}") from exc
|
|
709
|
+
cases = identity["cases"]
|
|
710
|
+
if len(judgments) != len(cases):
|
|
711
|
+
raise ResultError("judgment count does not match evaluated cases")
|
|
712
|
+
if any(not isinstance(row, dict) or row.get("schema_version") != JUDGMENT_SCHEMA_VERSION
|
|
713
|
+
or row.get("run_id") != ref.run_id or row.get("evaluation_id") != ref.evaluation_id
|
|
714
|
+
or row.get("case_id") != case.get("case_id") or row.get("pool_digest") != case.get("pool_digest")
|
|
715
|
+
or not isinstance(row.get("values"), dict) or set(row["values"]) != set(_METRIC_IDS)
|
|
716
|
+
for row, case in zip(judgments, cases)):
|
|
717
|
+
raise ResultError("judgment schema, order, metric set, or identity mismatch")
|
|
718
|
+
if any(type(row.get("answerable")) is not bool or not isinstance(row.get("undefined_reasons"), dict)
|
|
719
|
+
or row.get("trial_index") != 1 or row.get("status") not in {
|
|
720
|
+
"scored", "candidate_failure", "corrupt", "incompatible", "unsupported_schema", "grader_error"
|
|
721
|
+
} for row in judgments):
|
|
722
|
+
raise ResultError("judgment status, eligibility, trial, or missingness is malformed")
|
|
723
|
+
if any(row["status"] != manifest["status"] for row in judgments) and manifest["status"] not in _GRADABLE:
|
|
724
|
+
raise ResultError("non-gradable judgment status differs from evaluation")
|
|
725
|
+
if manifest["status"] in _GRADABLE and any(row["status"] not in {"scored", "candidate_failure"} for row in judgments):
|
|
726
|
+
raise ResultError("gradable evaluation contains a non-gradable judgment")
|
|
727
|
+
results: list[CaseMetricResult] = []
|
|
728
|
+
for row in judgments:
|
|
729
|
+
terminal = "ok" if row["status"] == "scored" else row["candidate_status"] if row["status"] == "candidate_failure" else row["status"]
|
|
730
|
+
if not isinstance(terminal, str) or not terminal:
|
|
731
|
+
raise ResultError("judgment candidate terminal status is missing")
|
|
732
|
+
results.append(CaseMetricResult(
|
|
733
|
+
row["case_id"], row["pool_digest"], terminal, row["answerable"],
|
|
734
|
+
tuple(row["values"].items()), tuple(row["undefined_reasons"].items()),
|
|
735
|
+
))
|
|
736
|
+
try:
|
|
737
|
+
recomputed = aggregate_case_metrics(results)
|
|
738
|
+
except (TypeError, ValueError, KeyError) as exc:
|
|
739
|
+
raise ResultError(f"judgment metric facts are malformed: {exc}") from exc
|
|
740
|
+
if metrics["metrics"] != {key: value.to_dict() for key, value in recomputed.items()}:
|
|
741
|
+
raise ResultError("aggregate metrics differ from case judgments")
|
|
742
|
+
primary = recomputed["ndcg_at_5"]
|
|
743
|
+
expected_counts = {
|
|
744
|
+
"attempted_count": len(pack.cases) if manifest["status"] in _GRADABLE else None,
|
|
745
|
+
"success_count": sum(row["status"] == "scored" for row in judgments) if manifest["status"] in _GRADABLE else None,
|
|
746
|
+
"failure_count": sum(row["status"] == "candidate_failure" for row in judgments) if manifest["status"] in _GRADABLE else None,
|
|
747
|
+
"eligible_count": primary.eligible_count,
|
|
748
|
+
"observed_count": primary.observed_count,
|
|
749
|
+
"missing_count": primary.missing_count,
|
|
750
|
+
"undefined_count": primary.undefined_count,
|
|
751
|
+
}
|
|
752
|
+
if counts != expected_counts:
|
|
753
|
+
raise ResultError("evaluation counts differ from case judgments")
|
|
754
|
+
if manifest["status"] == "completed" and expected_counts["failure_count"] != 0:
|
|
755
|
+
raise ResultError("completed evaluation contains candidate failures")
|
|
756
|
+
if manifest["status"] == "incomplete" and expected_counts["failure_count"] == 0:
|
|
757
|
+
raise ResultError("incomplete evaluation has no candidate failure")
|
|
758
|
+
if manifest["status"] in _GRADABLE:
|
|
759
|
+
run_path = ref.path.parent.parent
|
|
760
|
+
try:
|
|
761
|
+
run_manifest = _json_object(run_path / "manifest.json")
|
|
762
|
+
source_ref = RunRef(ref.run_id, run_path, PACK_ID)
|
|
763
|
+
_check_root(source_ref, run_manifest, pack)
|
|
764
|
+
source_results, source_judgments, source_successes = _capture_results(source_ref, run_manifest, pack)
|
|
765
|
+
source_aggregates = aggregate_case_metrics(source_results)
|
|
766
|
+
except (_Ungradable, EvidenceError, ResultError, TypeError, ValueError, KeyError) as exc:
|
|
767
|
+
raise ResultError(f"evaluation source run failed verification: {exc}") from exc
|
|
768
|
+
source_status = "completed" if source_successes == len(pack.cases) else "incomplete"
|
|
769
|
+
source_reason = None if source_status == "completed" else f"{len(pack.cases) - source_successes} candidate attempt(s) failed"
|
|
770
|
+
if (manifest["status"], manifest["reason"]) != (source_status, source_reason):
|
|
771
|
+
raise ResultError("evaluation status or reason differs from verified source captures")
|
|
772
|
+
for source_judgment in source_judgments:
|
|
773
|
+
source_judgment["evaluation_id"] = ref.evaluation_id
|
|
774
|
+
if judgments != source_judgments:
|
|
775
|
+
raise ResultError("evaluation case judgments differ from verified raw captures and installed pack")
|
|
776
|
+
if metrics["metrics"] != {key: value.to_dict() for key, value in source_aggregates.items()}:
|
|
777
|
+
raise ResultError("evaluation aggregate metrics differ from verified source captures")
|
|
778
|
+
if candidate != run_manifest["candidate"]:
|
|
779
|
+
raise ResultError("evaluation candidate identity differs from source run")
|
|
780
|
+
if identity != _identity(run_manifest, pack):
|
|
781
|
+
raise ResultError("evaluation runner, execution policy, or case identity differs from source run")
|
|
782
|
+
source_primary = source_aggregates["ndcg_at_5"]
|
|
783
|
+
source_counts = {
|
|
784
|
+
"attempted_count": len(pack.cases),
|
|
785
|
+
"success_count": source_successes,
|
|
786
|
+
"failure_count": len(pack.cases) - source_successes,
|
|
787
|
+
"eligible_count": source_primary.eligible_count,
|
|
788
|
+
"observed_count": source_primary.observed_count,
|
|
789
|
+
"missing_count": source_primary.missing_count,
|
|
790
|
+
"undefined_count": source_primary.undefined_count,
|
|
791
|
+
}
|
|
792
|
+
if counts != source_counts:
|
|
793
|
+
raise ResultError("evaluation counts differ from verified source captures")
|
|
794
|
+
return ref
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def report(evaluation_ref: EvaluationRef | str | Path) -> Scorecard:
|
|
798
|
+
"""Return a JSON/Markdown scorecard for a hash-verified evaluation."""
|
|
799
|
+
|
|
800
|
+
ref = verify_evaluation(evaluation_ref)
|
|
801
|
+
assert ref.path is not None
|
|
802
|
+
data = _json_object(ref.path / "metrics.json")
|
|
803
|
+
latency = data["metrics"]["runner_latency_ms"]
|
|
804
|
+
data["runner_latency"] = {
|
|
805
|
+
"unit": latency["unit"],
|
|
806
|
+
"p50": latency["value"],
|
|
807
|
+
"p95": latency["p95"],
|
|
808
|
+
"eligible_count": latency["eligible_count"],
|
|
809
|
+
"observed_count": latency["observed_count"],
|
|
810
|
+
"missing_count": latency["missing_count"],
|
|
811
|
+
"complete": latency["complete"],
|
|
812
|
+
}
|
|
813
|
+
return Scorecard(data)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _ref_or_error(value: object) -> tuple[EvaluationRef | None, str | None]:
|
|
817
|
+
if isinstance(value, FastSearchSmokeResult):
|
|
818
|
+
return None, "Fast Search smoke/latency result has no labeled frozen-pool quality evaluation"
|
|
819
|
+
if not isinstance(value, (EvaluationRef, str, os.PathLike)):
|
|
820
|
+
return None, f"unsupported evaluation reference type: {type(value).__name__}"
|
|
821
|
+
try:
|
|
822
|
+
return verify_evaluation(value), None
|
|
823
|
+
except (ResultError, TypeError, OSError) as exc:
|
|
824
|
+
return None, str(exc)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def compare(evaluation_ref_a: object, evaluation_ref_b: object) -> Comparison:
|
|
828
|
+
"""Compare only matching saved workloads and report paired B-minus-A deltas."""
|
|
829
|
+
|
|
830
|
+
ref_a, error_a = _ref_or_error(evaluation_ref_a)
|
|
831
|
+
ref_b, error_b = _ref_or_error(evaluation_ref_b)
|
|
832
|
+
name_a = ref_a.evaluation_id if ref_a else evaluation_ref_a.evaluation_id if isinstance(evaluation_ref_a, EvaluationRef) else None
|
|
833
|
+
name_b = ref_b.evaluation_id if ref_b else evaluation_ref_b.evaluation_id if isinstance(evaluation_ref_b, EvaluationRef) else None
|
|
834
|
+
base: dict[str, Any] = {
|
|
835
|
+
"schema_version": "sphynx.comparison.v1",
|
|
836
|
+
"evaluation_a": name_a,
|
|
837
|
+
"evaluation_b": name_b,
|
|
838
|
+
"compatible": False,
|
|
839
|
+
"reason": None,
|
|
840
|
+
"trust_tier": "local_unverified",
|
|
841
|
+
"illustrative": True,
|
|
842
|
+
"metrics": {},
|
|
843
|
+
}
|
|
844
|
+
if error_a or error_b:
|
|
845
|
+
base["reason"] = f"evaluation {'A' if error_a else 'B'} failed integrity validation: {error_a or error_b}"
|
|
846
|
+
return Comparison(base)
|
|
847
|
+
assert ref_a is not None and ref_b is not None and ref_a.path is not None and ref_b.path is not None
|
|
848
|
+
a = _json_object(ref_a.path / "metrics.json")
|
|
849
|
+
b = _json_object(ref_b.path / "metrics.json")
|
|
850
|
+
rows_a = [json.loads(line) for line in (ref_a.path / "judgments.jsonl").read_text(encoding="utf-8").splitlines()]
|
|
851
|
+
rows_b = [json.loads(line) for line in (ref_b.path / "judgments.jsonl").read_text(encoding="utf-8").splitlines()]
|
|
852
|
+
return Comparison(compare_validated_records(a, b, rows_a, rows_b))
|