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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Storage-independent paired arithmetic and validated RAG comparisons.
|
|
2
|
+
|
|
3
|
+
This module does not read files, resolve references, authenticate users, or run
|
|
4
|
+
candidates. Its caller must first verify the records against their evidence and
|
|
5
|
+
installed releases. The local results adapter performs that verification before
|
|
6
|
+
calling this function; a hosted adapter must provide its own equivalent gate.
|
|
7
|
+
|
|
8
|
+
The metric-only helper assigns no trust tier or report schema. The full-record
|
|
9
|
+
adapter retains the existing illustrative reranking comparison semantics. This
|
|
10
|
+
module is not a grader or a newly trusted hosted result schema.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComparisonInputError(ValueError):
|
|
20
|
+
"""Validated comparison records were supplied with misaligned case rows."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compare_validated_records(
|
|
24
|
+
a: Mapping[str, Any],
|
|
25
|
+
b: Mapping[str, Any],
|
|
26
|
+
rows_a: Sequence[Mapping[str, Any]],
|
|
27
|
+
rows_b: Sequence[Mapping[str, Any]],
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""Return the existing comparison record, including paired B-minus-A deltas.
|
|
30
|
+
|
|
31
|
+
``a`` and ``b`` have the verified ``metrics.json`` shape. Their ordered case
|
|
32
|
+
judgments are supplied separately, so storage and execution are irrelevant
|
|
33
|
+
to the calculation. Candidate identities may differ; the workload, grading,
|
|
34
|
+
execution, and metric identities must match. Incomplete eligible pairs never
|
|
35
|
+
become zero, and a complete mean is null if any eligible pair is missing.
|
|
36
|
+
|
|
37
|
+
Latency uses each case's runner wall span, including unsupported questions.
|
|
38
|
+
Its comparison is an arithmetic mean of paired differences, not a difference
|
|
39
|
+
of aggregate p50/p95 values and not an inference-only latency claim.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
base: dict[str, Any] = {
|
|
43
|
+
"schema_version": "sphynx.comparison.v1",
|
|
44
|
+
"evaluation_a": a["evaluation_id"],
|
|
45
|
+
"evaluation_b": b["evaluation_id"],
|
|
46
|
+
"compatible": False,
|
|
47
|
+
"reason": None,
|
|
48
|
+
"trust_tier": "local_unverified",
|
|
49
|
+
"illustrative": True,
|
|
50
|
+
"metrics": {},
|
|
51
|
+
}
|
|
52
|
+
if a["status"] not in {"completed", "incomplete"} or b["status"] not in {"completed", "incomplete"}:
|
|
53
|
+
base["reason"] = f"non-gradable evaluation status: A={a['status']}, B={b['status']}"
|
|
54
|
+
return base
|
|
55
|
+
for field in ("pack_id", "track", "trust_tier", "releases", "identity", "metric_definitions"):
|
|
56
|
+
if a.get(field) != b.get(field):
|
|
57
|
+
if field == "identity":
|
|
58
|
+
for subfield in ("pack_id", "pack_release", "track", "cases", "execution_policy", "runner", "trust_tier"):
|
|
59
|
+
if a[field].get(subfield) != b[field].get(subfield):
|
|
60
|
+
base["reason"] = f"comparison identity mismatch: {subfield}"
|
|
61
|
+
return base
|
|
62
|
+
base["reason"] = f"comparison identity mismatch: {field}"
|
|
63
|
+
return base
|
|
64
|
+
|
|
65
|
+
# A storage adapter must never accidentally pair reordered or truncated rows.
|
|
66
|
+
# This checks the adapter boundary; it does not replace evidence verification.
|
|
67
|
+
cases = a["identity"]["cases"]
|
|
68
|
+
if len(rows_a) != len(cases) or len(rows_b) != len(cases):
|
|
69
|
+
raise ComparisonInputError("judgment count does not match evaluated cases")
|
|
70
|
+
for row_a, row_b, case in zip(rows_a, rows_b, cases):
|
|
71
|
+
if any(row.get("case_id") != case.get("case_id") or row.get("pool_digest") != case.get("pool_digest")
|
|
72
|
+
for row in (row_a, row_b)):
|
|
73
|
+
raise ComparisonInputError("judgment order or case/pool identity mismatch")
|
|
74
|
+
if any(type(row.get("answerable")) is not bool for row in (row_a, row_b)) or row_a["answerable"] != row_b["answerable"]:
|
|
75
|
+
raise ComparisonInputError("judgment eligibility differs between paired cases")
|
|
76
|
+
|
|
77
|
+
base["compatible"] = True
|
|
78
|
+
base["candidate_a"] = a["candidate"]
|
|
79
|
+
base["candidate_b"] = b["candidate"]
|
|
80
|
+
base["releases"] = a["releases"]
|
|
81
|
+
base["metrics"] = paired_metric_deltas(a["metric_definitions"], rows_a, rows_b)
|
|
82
|
+
return base
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def paired_metric_deltas(
|
|
86
|
+
metric_definitions: Sequence[Mapping[str, Any]],
|
|
87
|
+
rows_a: Sequence[Mapping[str, Any]],
|
|
88
|
+
rows_b: Sequence[Mapping[str, Any]],
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""Calculate metric records without assigning a report schema or trust tier.
|
|
91
|
+
|
|
92
|
+
The trusted caller must authenticate and verify the evidence, finite metric
|
|
93
|
+
values, grading releases, and comparison compatibility before calling. This
|
|
94
|
+
helper only checks paired row alignment and computes arithmetic; it cannot
|
|
95
|
+
make unverified records trustworthy. It performs no storage or execution.
|
|
96
|
+
|
|
97
|
+
Rows use the current per-case metric contract: case_id, pool_digest,
|
|
98
|
+
answerable, status, and values. The existing unsupported-query eligibility
|
|
99
|
+
rule applies to quality metrics; runner_latency_ms includes every case.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
if len(rows_a) != len(rows_b):
|
|
103
|
+
raise ComparisonInputError("judgment counts differ between paired records")
|
|
104
|
+
for row_a, row_b in zip(rows_a, rows_b):
|
|
105
|
+
if any(row_a.get(field) != row_b.get(field) for field in ("case_id", "pool_digest")):
|
|
106
|
+
raise ComparisonInputError("judgment order or case/pool identity mismatch")
|
|
107
|
+
if any(type(row.get("answerable")) is not bool for row in (row_a, row_b)) or row_a["answerable"] != row_b["answerable"]:
|
|
108
|
+
raise ComparisonInputError("judgment eligibility differs between paired cases")
|
|
109
|
+
metrics: dict[str, Any] = {}
|
|
110
|
+
for definition in metric_definitions:
|
|
111
|
+
metric_id = definition["metric_id"]
|
|
112
|
+
per_case: list[dict[str, Any]] = []
|
|
113
|
+
deltas: list[float] = []
|
|
114
|
+
undefined_count = missing_count = 0
|
|
115
|
+
for row_a, row_b in zip(rows_a, rows_b):
|
|
116
|
+
value_a = row_a["values"][metric_id]
|
|
117
|
+
value_b = row_b["values"][metric_id]
|
|
118
|
+
undefined = not row_a["answerable"] and metric_id != "runner_latency_ms"
|
|
119
|
+
if undefined:
|
|
120
|
+
undefined_count += 1
|
|
121
|
+
delta = None
|
|
122
|
+
reason = "unsupported_query"
|
|
123
|
+
elif value_a is None or value_b is None:
|
|
124
|
+
missing_count += 1
|
|
125
|
+
delta = None
|
|
126
|
+
reason = f"missing value: A={row_a['status'] if value_a is None else 'observed'}, " \
|
|
127
|
+
f"B={row_b['status'] if value_b is None else 'observed'}"
|
|
128
|
+
else:
|
|
129
|
+
delta = value_b - value_a
|
|
130
|
+
deltas.append(delta)
|
|
131
|
+
reason = None
|
|
132
|
+
per_case.append({
|
|
133
|
+
"case_id": row_a["case_id"], "value_a": value_a, "value_b": value_b,
|
|
134
|
+
"delta_b_minus_a": delta, "reason": reason,
|
|
135
|
+
})
|
|
136
|
+
metrics[metric_id] = {
|
|
137
|
+
"unit": definition["unit"],
|
|
138
|
+
"direction": definition["direction"],
|
|
139
|
+
"primary": definition["primary"],
|
|
140
|
+
"eligible_count": len(rows_a) - undefined_count,
|
|
141
|
+
"paired_count": len(deltas),
|
|
142
|
+
"missing_pair_count": missing_count,
|
|
143
|
+
"undefined_count": undefined_count,
|
|
144
|
+
"complete": missing_count == 0 and bool(deltas),
|
|
145
|
+
"mean_delta": sum(deltas) / len(deltas) if missing_count == 0 and deltas else None,
|
|
146
|
+
"observed_mean_delta": sum(deltas) / len(deltas) if deltas else None,
|
|
147
|
+
"per_case": per_case,
|
|
148
|
+
}
|
|
149
|
+
return metrics
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Immutable permitted comparison views; native policy remains authoritative."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .evaluation_protocols import _envelope
|
|
11
|
+
from .workflow_contracts import (
|
|
12
|
+
WorkflowContractError, _object, _optional_text, _text, _thaw, contract_digest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
VIEW_SCHEMA = "sphynx.comparison-view.v1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _number(value: Any, name: str) -> None:
|
|
19
|
+
if value is not None and (type(value) not in (int, float) or not math.isfinite(value)):
|
|
20
|
+
raise WorkflowContractError(name + " must be a finite number or null")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _items(value, cls, name):
|
|
24
|
+
if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
|
|
25
|
+
raise WorkflowContractError(name + " must be a sequence")
|
|
26
|
+
values = tuple(value)
|
|
27
|
+
if any(not isinstance(item, cls) for item in values):
|
|
28
|
+
raise WorkflowContractError(name + " contains an invalid typed value")
|
|
29
|
+
return values
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _decode(cls, value):
|
|
33
|
+
if not isinstance(value, Mapping):
|
|
34
|
+
raise WorkflowContractError("comparison item must be an object")
|
|
35
|
+
try:
|
|
36
|
+
return cls(**dict(value))
|
|
37
|
+
except TypeError as exc:
|
|
38
|
+
raise WorkflowContractError("invalid comparison item fields") from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ComparisonPair:
|
|
43
|
+
"""A native observation key and its values; keys do not imply independence."""
|
|
44
|
+
|
|
45
|
+
key: Mapping[str, Any]
|
|
46
|
+
baseline: float | bool | None
|
|
47
|
+
candidate: float | bool | None
|
|
48
|
+
delta: float | None
|
|
49
|
+
availability: str
|
|
50
|
+
reason: str | None = None
|
|
51
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
object.__setattr__(self, "key", _object(self.key, "pair key"))
|
|
55
|
+
if not self.key:
|
|
56
|
+
raise WorkflowContractError("pair requires an explicit observation key")
|
|
57
|
+
for name in ("baseline", "candidate", "delta"):
|
|
58
|
+
if name == "delta" or type(getattr(self, name)) is not bool:
|
|
59
|
+
_number(getattr(self, name), name)
|
|
60
|
+
if self.availability not in ("available", "missing", "undefined", "unavailable"):
|
|
61
|
+
raise WorkflowContractError("invalid pair availability")
|
|
62
|
+
_optional_text(self.reason, "pair reason")
|
|
63
|
+
if self.availability == "available":
|
|
64
|
+
if any(getattr(self, name) is None for name in ("baseline", "candidate", "delta")):
|
|
65
|
+
raise WorkflowContractError("available pair requires both values and native delta")
|
|
66
|
+
elif self.delta is not None or self.reason is None:
|
|
67
|
+
raise WorkflowContractError("unavailable pair requires a reason and null delta")
|
|
68
|
+
object.__setattr__(self, "metadata", _object(self.metadata, "pair metadata"))
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {"key": _thaw(self.key), "baseline": self.baseline, "candidate": self.candidate,
|
|
72
|
+
"delta": self.delta, "availability": self.availability, "reason": self.reason,
|
|
73
|
+
"metadata": _thaw(self.metadata)}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ComparisonMetric:
|
|
78
|
+
"""Native metric meaning, complete/observed deltas and coverage."""
|
|
79
|
+
|
|
80
|
+
metric_id: str
|
|
81
|
+
unit: str
|
|
82
|
+
direction: str
|
|
83
|
+
delta_convention: str
|
|
84
|
+
status: str
|
|
85
|
+
population: Mapping[str, Any]
|
|
86
|
+
method: Mapping[str, Any]
|
|
87
|
+
delta: float | None = None
|
|
88
|
+
observed_delta: float | None = None
|
|
89
|
+
counts: Mapping[str, int] = field(default_factory=dict)
|
|
90
|
+
pairs: tuple[ComparisonPair, ...] = ()
|
|
91
|
+
uncertainty: Mapping[str, Any] | None = None
|
|
92
|
+
reason: str | None = None
|
|
93
|
+
|
|
94
|
+
def __post_init__(self):
|
|
95
|
+
for name in ("metric_id", "unit", "direction", "delta_convention"):
|
|
96
|
+
_text(getattr(self, name), name)
|
|
97
|
+
if self.status not in ("supported", "incomplete", "unavailable"):
|
|
98
|
+
raise WorkflowContractError("invalid comparison metric status")
|
|
99
|
+
for name in ("delta", "observed_delta"):
|
|
100
|
+
_number(getattr(self, name), name)
|
|
101
|
+
_optional_text(self.reason, "metric reason")
|
|
102
|
+
if (self.status == "supported") != (self.delta is not None):
|
|
103
|
+
raise WorkflowContractError("metric status and complete-population delta disagree")
|
|
104
|
+
if self.status == "unavailable" and (self.observed_delta is not None or self.reason is None):
|
|
105
|
+
raise WorkflowContractError("unavailable metric requires reason and null observed delta")
|
|
106
|
+
for name in ("population", "method", "counts"):
|
|
107
|
+
object.__setattr__(self, name, _object(getattr(self, name), name))
|
|
108
|
+
if not self.population or not self.method:
|
|
109
|
+
raise WorkflowContractError("metric requires explicit population and method")
|
|
110
|
+
if any(type(value) is not int or value < 0 for value in self.counts.values()):
|
|
111
|
+
raise WorkflowContractError("comparison counts must be nonnegative integers")
|
|
112
|
+
pairs = _items(self.pairs, ComparisonPair, "pairs")
|
|
113
|
+
if len({contract_digest(pair.key) for pair in pairs}) != len(pairs):
|
|
114
|
+
raise WorkflowContractError("duplicate comparison observation key")
|
|
115
|
+
object.__setattr__(self, "pairs", pairs)
|
|
116
|
+
if self.uncertainty is not None:
|
|
117
|
+
object.__setattr__(self, "uncertainty", _object(self.uncertainty, "uncertainty"))
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict[str, Any]:
|
|
120
|
+
return {"metric_id": self.metric_id, "unit": self.unit, "direction": self.direction,
|
|
121
|
+
"delta_convention": self.delta_convention, "status": self.status,
|
|
122
|
+
"population": _thaw(self.population), "method": _thaw(self.method),
|
|
123
|
+
"delta": self.delta, "observed_delta": self.observed_delta,
|
|
124
|
+
"counts": _thaw(self.counts), "pairs": [pair.to_dict() for pair in self.pairs],
|
|
125
|
+
"uncertainty": None if self.uncertainty is None else _thaw(self.uncertainty),
|
|
126
|
+
"reason": self.reason}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class ComparisonView:
|
|
131
|
+
"""Readable native comparison projection, with no rank or acceptance verdict."""
|
|
132
|
+
|
|
133
|
+
baseline: Mapping[str, Any]
|
|
134
|
+
candidate: Mapping[str, Any]
|
|
135
|
+
backend: Mapping[str, Any]
|
|
136
|
+
status: str
|
|
137
|
+
native: Mapping[str, Any] = field(repr=False)
|
|
138
|
+
metrics: tuple[ComparisonMetric, ...] = ()
|
|
139
|
+
reason: str | None = None
|
|
140
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
def __post_init__(self):
|
|
143
|
+
for name in ("baseline", "candidate", "backend", "native", "metadata"):
|
|
144
|
+
object.__setattr__(self, name, _object(getattr(self, name), name))
|
|
145
|
+
for source in (self.baseline, self.candidate):
|
|
146
|
+
for name in ("grading_id", "status", "trust_tier"):
|
|
147
|
+
_text(source.get(name), "comparison source " + name)
|
|
148
|
+
_optional_text(source.get("run_id"), "comparison source run_id")
|
|
149
|
+
_text(self.backend.get("backend_id"), "comparison backend_id")
|
|
150
|
+
if self.status not in ("supported", "incompatible", "incomplete", "unavailable"):
|
|
151
|
+
raise WorkflowContractError("invalid comparison status")
|
|
152
|
+
_optional_text(self.reason, "comparison reason")
|
|
153
|
+
metrics = _items(self.metrics, ComparisonMetric, "metrics")
|
|
154
|
+
if len({metric.metric_id for metric in metrics}) != len(metrics):
|
|
155
|
+
raise WorkflowContractError("duplicate comparison metric")
|
|
156
|
+
if self.status in ("incompatible", "unavailable") and (metrics or self.reason is None):
|
|
157
|
+
raise WorkflowContractError("refused/unavailable view requires reason and no metrics")
|
|
158
|
+
if self.status == "supported" and any(metric.status == "incomplete" for metric in metrics):
|
|
159
|
+
raise WorkflowContractError("supported view cannot hide incomplete metrics")
|
|
160
|
+
object.__setattr__(self, "metrics", metrics)
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> dict[str, Any]:
|
|
163
|
+
return {"schema_version": VIEW_SCHEMA, "baseline": _thaw(self.baseline),
|
|
164
|
+
"candidate": _thaw(self.candidate), "backend": _thaw(self.backend),
|
|
165
|
+
"status": self.status, "reason": self.reason,
|
|
166
|
+
"metrics": [metric.to_dict() for metric in self.metrics],
|
|
167
|
+
"metadata": _thaw(self.metadata), "native": _thaw(self.native)}
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ComparisonView:
|
|
171
|
+
values = _envelope(value, VIEW_SCHEMA, cls)
|
|
172
|
+
raw_metrics = values.get("metrics", ())
|
|
173
|
+
if isinstance(raw_metrics, (str, bytes)) or not isinstance(raw_metrics, Sequence):
|
|
174
|
+
raise WorkflowContractError("metrics must be an array")
|
|
175
|
+
metrics = []
|
|
176
|
+
for raw in raw_metrics:
|
|
177
|
+
if not isinstance(raw, Mapping):
|
|
178
|
+
raise WorkflowContractError("metric must be an object")
|
|
179
|
+
item = dict(raw)
|
|
180
|
+
pairs = item.get("pairs", ())
|
|
181
|
+
if isinstance(pairs, (str, bytes)) or not isinstance(pairs, Sequence):
|
|
182
|
+
raise WorkflowContractError("pairs must be an array")
|
|
183
|
+
item["pairs"] = tuple(_decode(ComparisonPair, pair) for pair in pairs)
|
|
184
|
+
metrics.append(_decode(ComparisonMetric, item))
|
|
185
|
+
values["metrics"] = tuple(metrics)
|
|
186
|
+
return _decode(cls, values)
|
|
187
|
+
|
|
188
|
+
def to_json(self) -> str:
|
|
189
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, allow_nan=False, sort_keys=True)
|
|
190
|
+
|
|
191
|
+
def to_markdown(self) -> str:
|
|
192
|
+
def cell(value):
|
|
193
|
+
return str(value).replace("|", "\\|").replace("\n", " ").replace("\r", " ")
|
|
194
|
+
lines = ["# Evaluation comparison", "",
|
|
195
|
+
"Baseline: " + cell(self.baseline["grading_id"]),
|
|
196
|
+
"Candidate: " + cell(self.candidate["grading_id"]),
|
|
197
|
+
"Status: " + self.status]
|
|
198
|
+
if self.reason:
|
|
199
|
+
lines.append("Reason: " + cell(self.reason))
|
|
200
|
+
if self.metrics:
|
|
201
|
+
lines += ["", "| Metric | Unit / direction | Complete delta | Observed delta | Coverage |",
|
|
202
|
+
"| --- | --- | ---: | ---: | --- |"]
|
|
203
|
+
for metric in self.metrics:
|
|
204
|
+
lines.append("| " + " | ".join(cell(value) for value in (
|
|
205
|
+
metric.metric_id, metric.unit + " / " + metric.direction,
|
|
206
|
+
metric.delta if metric.delta is not None else "unavailable",
|
|
207
|
+
metric.observed_delta if metric.observed_delta is not None else "unavailable",
|
|
208
|
+
json.dumps(_thaw(metric.counts), sort_keys=True))) + " |")
|
|
209
|
+
lines.append("")
|
|
210
|
+
for metric in self.metrics:
|
|
211
|
+
lines.append(cell(metric.metric_id + ": " + metric.delta_convention + "; " +
|
|
212
|
+
json.dumps(_thaw(metric.method), sort_keys=True)))
|
|
213
|
+
lines.append("Status: " + metric.status)
|
|
214
|
+
if metric.reason:
|
|
215
|
+
lines.append("Reason: " + cell(metric.reason))
|
|
216
|
+
lines.append("Uncertainty: " + (cell(json.dumps(_thaw(metric.uncertainty), sort_keys=True))
|
|
217
|
+
if metric.uncertainty is not None else "not estimated by the native policy"))
|
|
218
|
+
reasons = sorted({pair.reason for pair in metric.pairs if pair.reason is not None})
|
|
219
|
+
if reasons:
|
|
220
|
+
lines.append("Missing/undefined pair reasons: " + "; ".join(cell(reason) for reason in reasons))
|
|
221
|
+
lines += ["", "Native policy controls compatibility. No rank or acceptance verdict is inferred.",
|
|
222
|
+
"Use to_dict() for populations, case/trial pairs, limitations and native detail."]
|
|
223
|
+
return "\n".join(lines) + "\n"
|