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.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. 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"