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
sphynx/metric_views.py ADDED
@@ -0,0 +1,474 @@
1
+ """Small, aggregate-only views of the versioned metric catalog.
2
+
3
+ This is a presentation layer, not a grader or publication authorization.
4
+ Pass verified SDK scorecards; full evidence and scoring remain unchanged.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import math
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Mapping
13
+
14
+ _PRIORITY_SOURCE = "docs/reports/sable-offline-evals-metric-priority.md"
15
+ _PROPOSAL_SOURCE = "docs/multidomain-evaluation-proposal.md"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class MetricCard:
20
+ metric_id: str
21
+ label: str
22
+ direction: str
23
+ unit: str
24
+ role: str
25
+ eligible_population: str
26
+ description: str
27
+ implementation: str
28
+ source: str
29
+
30
+ def to_dict(self) -> dict[str, Any]:
31
+ return vars(self).copy()
32
+
33
+
34
+ def _card(metric_id: str, label: str, direction: str, population: str,
35
+ description: str, *, unit: str = "ratio", role: str = "diagnostic",
36
+ planned: bool = False, source: str = _PRIORITY_SOURCE) -> MetricCard:
37
+ return MetricCard(metric_id, label, direction, unit, role, population, description,
38
+ "planned" if planned else "available", source)
39
+
40
+
41
+ _COVERAGE = _card("valid_response_coverage", "Valid response coverage", "higher",
42
+ "all attempted cases", "Valid candidate responses divided by attempted cases.", role="reliability")
43
+ _LATENCY_P50 = _card("runner_latency_p50_ms", "Runner latency p50", "lower",
44
+ "attempted cases with captured runner spans",
45
+ "Median runner wall time, including process and adapter overhead.", unit="ms", role="efficiency")
46
+ _LATENCY_P95 = _card("runner_latency_p95_ms", "Runner latency p95", "lower",
47
+ "attempted cases with captured runner spans",
48
+ "95th percentile runner wall time; not isolated model inference time.", unit="ms", role="efficiency")
49
+
50
+ _CATALOG: dict[str, tuple[MetricCard, ...]] = {
51
+ "rag.invoke": (
52
+ _card("three_way_accuracy", "Routing accuracy", "higher", "all invocation cases",
53
+ "Correct choice of CALL_RAG, ASK_BUYER, or ANSWER_FROM_CONTEXT.", role="primary",
54
+ source="docs/rag-invocation-v1.md"),
55
+ _card("required_lookup_recall", "Required lookup recall", "higher", "CALL_RAG gold cases",
56
+ "Required lookup opportunities on which the candidate calls RAG."),
57
+ _card("unnecessary_call_rate", "Unnecessary call rate", "lower", "non-CALL_RAG gold cases",
58
+ "Calls on opportunities labeled clarification or answer from context."),
59
+ _card("clarification_accuracy", "Clarification accuracy", "higher", "ASK_BUYER gold cases",
60
+ "Required clarification opportunities on which the candidate asks."),
61
+ _card("answer_from_context_accuracy", "Answer from context accuracy", "higher",
62
+ "ANSWER_FROM_CONTEXT gold cases", "Context-sufficient opportunities answered without retrieval."),
63
+ _COVERAGE, _LATENCY_P50, _LATENCY_P95,
64
+ ),
65
+ "rag.retrieve": (
66
+ _card("complete_evidence_success", "Complete evidence in context", "higher",
67
+ "all corpus-answerable retrieval questions",
68
+ "All required reference groups reach exact final context within the declared UTF-8 byte budget; provisional anchors are recovery proxies.", role="primary"),
69
+ _card("candidate_evidence_recall_at_50", "Candidate evidence recall@50", "higher",
70
+ "all corpus-answerable retrieval questions",
71
+ "Required reference groups recovered in the first-stage candidate pool of at most fifty."),
72
+ _card("final_evidence_group_recall", "Final evidence coverage", "higher",
73
+ "all corpus-answerable retrieval questions",
74
+ "Required reference groups represented in the exact context delivered by the candidate."),
75
+ _card("ndcg_at_5", "nDCG@5", "higher", "all corpus-answerable retrieval questions",
76
+ "Final reranked top-five ranking quality normalized against corpus-wide reference relevance, including genuine retrieval misses."),
77
+ _card("grounded_answer_success", "Grounded answer success", "higher",
78
+ "answerable and fallback opportunities",
79
+ "Requires a separately versioned answer and claim-support grader; not computed by retrieval capture.", planned=True),
80
+ _card("retrieval_latency_p95_ms", "Retrieval and assembly p95", "lower",
81
+ "attempted cases with SDK-observed adapter spans",
82
+ "Actual per-case adapter wall time; excludes index construction. Caller-supplied component timing is diagnostic only.", unit="ms", role="efficiency"),
83
+ _card("cost_per_evidence_complete_query", "Cost per evidence-complete query", "lower",
84
+ "evidence-complete queries with complete cost evidence",
85
+ "Unavailable without a versioned verified pricing and resource accounting policy; no inferred zero-cost local runs.", unit="USD/query", role="efficiency"),
86
+ _COVERAGE,
87
+ ),
88
+ "rag.rerank": (
89
+ _card("ndcg_at_5", "nDCG@5", "higher", "answerable, gold-present frozen pools",
90
+ "Top-five graded ranking quality normalized by the ideal ranking.", role="primary",
91
+ source="src/sphynx/metrics.py"),
92
+ _card("hit_at_5", "Hit@5", "higher", "answerable frozen pools",
93
+ "Whether a relevance-grade 2 or 3 passage appears in the first five.", source="src/sphynx/metrics.py"),
94
+ _card("mrr_at_5", "MRR@5", "higher", "answerable frozen pools",
95
+ "Reciprocal rank of the first relevance-grade 2 or 3 passage within five.", source="src/sphynx/metrics.py"),
96
+ _card("evidence_group_recall_at_5", "Evidence group recall@5", "higher", "pools with required evidence groups",
97
+ "Fraction of required evidence groups represented among the top five.", source="src/sphynx/metrics.py"),
98
+ _COVERAGE,
99
+ _card("candidate_failure_rate", "Candidate failure rate", "lower", "all attempted cases",
100
+ "Failed, timed out, or invalid candidate responses divided by attempted cases.", role="reliability"),
101
+ _LATENCY_P50, _LATENCY_P95,
102
+ ),
103
+ }
104
+
105
+ # Paired development grading; semantic credit requires capture-bound owner judgments.
106
+ _ROBUSTNESS_SOURCE = "docs/question-robustness.md"
107
+ _CATALOG["rag.question_robustness"] = (
108
+ _card("required_lookup_recall", "Required lookup recall", "higher",
109
+ "reviewed required-lookup opportunities, sliced by utterance variant",
110
+ "Correct RAG calls divided by required-lookup opportunities; direct, embedded, implicit and distractor variants remain separate.",
111
+ role="primary", source=_ROBUSTNESS_SOURCE),
112
+ _card("unnecessary_call_rate", "Unnecessary call rate", "lower",
113
+ "reviewed non-lookup controls",
114
+ "RAG calls on reviewed context-sufficient, non-request or clarification controls.",
115
+ source=_ROBUSTNESS_SOURCE),
116
+ _card("query_intent_preservation", "Query intent preservation", "higher",
117
+ "eligible emitted query sets on required-lookup cases",
118
+ "Owner-reviewed preservation of the information need across the full query set; unavailable without captured queries and versioned judgments.",
119
+ source=_ROBUSTNESS_SOURCE),
120
+ _card("required_constraint_coverage", "Required constraint coverage", "higher",
121
+ "eligible emitted required-lookup query sets with required constraints",
122
+ "Mean per-case fraction of required constraints retained, using owner judgments or versioned typed equivalence rules; lexical overlap is insufficient.",
123
+ source=_ROBUSTNESS_SOURCE),
124
+ _card("complete_evidence_success", "Complete evidence in context", "higher",
125
+ "all corpus-answerable required-lookup utterances",
126
+ "All required evidence groups reach exact final context; valid intentional skips score zero and provisional source anchors remain recovery proxies.",
127
+ role="primary", source=_ROBUSTNESS_SOURCE),
128
+ _card("paired_lookup_drop_pp", "Paired lookup drop", "lower",
129
+ "complete equivalent direct/variant pairs in the declared required-lookup families",
130
+ "Mean direct minus variant lookup success, in percentage points; negative drops are improvements and missing pairs make the primary comparison incomplete.",
131
+ unit="pp", source=_ROBUSTNESS_SOURCE),
132
+ _card("paired_evidence_drop_pp", "Paired evidence drop", "lower",
133
+ "complete equivalent direct/variant pairs in the declared corpus-answerable families",
134
+ "Mean direct minus variant complete-evidence success, in percentage points, with uncertainty clustered by semantic family.",
135
+ unit="pp", source=_ROBUSTNESS_SOURCE),
136
+ _card("valid_capture_coverage", "Valid capture coverage", "higher",
137
+ "all attempted robustness cases",
138
+ "Valid candidate outcomes divided by all attempted cases; validity does not imply correct routing, query meaning or evidence.",
139
+ role="reliability", source=_ROBUSTNESS_SOURCE),
140
+ )
141
+
142
+ _CATALOG["rag.rerank.public"] = (
143
+ _card("ndcg_at_5", "nDCG@5", "higher", "public pools with at least one positive qrel",
144
+ "Native graded gain normalized by the ideal frozen BM25 pool ranking.", role="primary", source="docs/public-reranking-v1.md"),
145
+ _card("hit_at_5", "Hit@5", "higher", "public pools with at least one positive qrel",
146
+ "At least one native relevance grade >0 passage in the first five.", source="docs/public-reranking-v1.md"),
147
+ _card("mrr_at_5", "MRR@5", "higher", "public pools with at least one positive qrel",
148
+ "Reciprocal rank of the first native positive qrel in the first five.", source="docs/public-reranking-v1.md"),
149
+ _card("pool_oracle_coverage", "BM25 pool oracle coverage", "higher", "all public questions",
150
+ "Fraction of questions whose frozen 15-passage pool includes any positive qrel.", source="docs/public-reranking-v1.md"),
151
+ _COVERAGE,
152
+ _card("candidate_failure_rate", "Candidate failure rate", "lower", "all attempted public questions",
153
+ "Failures and invalid responses divided by all attempts.", role="reliability"),
154
+ _LATENCY_P50, _LATENCY_P95,
155
+ )
156
+
157
+ # These public design cards are definitions only: no runner or grader is installed.
158
+ _PLANNED: dict[str, tuple[tuple[str, str, str, str, str], ...]] = {
159
+ "rag.answer": (
160
+ ("selective_task_utility", "Selective task utility", "higher", "utility", "answerable and fallback opportunities"),
161
+ ("unsupported_critical_claim_rate", "Unsupported critical claims", "lower", "ratio", "material answer claims"),
162
+ ("claim_support_precision", "Claim support precision", "higher", "ratio", "answer claims"),
163
+ ("required_claim_recall", "Required claim recall", "higher", "ratio", "required supported claims"),
164
+ ("citation_resolution_accuracy", "Citation resolution", "higher", "ratio", "cited source anchors"),
165
+ ("fallback_action_accuracy", "Fallback action accuracy", "higher", "ratio", "insufficient or conflicting context"),
166
+ ),
167
+ "highlighting": (
168
+ ("span_precision", "Span precision", "higher", "ratio", "predicted spans"),
169
+ ("span_recall", "Span recall", "higher", "ratio", "reference spans"),
170
+ ("span_overlap", "Span overlap", "higher", "ratio", "aligned reference and predicted spans"),
171
+ ("boundary_correctness", "Boundary correctness", "higher", "ratio", "highlight boundaries"),
172
+ ("semantic_correctness", "Semantic correctness", "higher", "ratio", "eligible highlighting cases"),
173
+ ),
174
+ "transcription": (
175
+ ("semantic_task_success", "Semantic task success", "higher", "ratio", "audio task cases"),
176
+ ("word_error_rate", "Word error rate", "lower", "ratio", "reference words"),
177
+ ("domain_entity_accuracy", "Domain entity accuracy", "higher", "ratio", "domain entities in speech"),
178
+ ("diarization_error_rate", "Diarization error", "lower", "ratio", "reference speaker time"),
179
+ ("timestamp_error_ms", "Timestamp error", "lower", "ms", "aligned words or segments"),
180
+ ("real_time_factor", "Real time factor", "lower", "ratio", "audio duration"),
181
+ ),
182
+ "tts": (
183
+ ("intelligibility", "Intelligibility", "higher", "rating", "synthesized speech cases"),
184
+ ("naturalness", "Naturalness", "higher", "rating", "synthesized speech cases"),
185
+ ("speaker_fidelity", "Speaker fidelity", "higher", "rating", "reference speaker cases"),
186
+ ("real_time_factor", "Real time factor", "lower", "ratio", "synthesized audio duration"),
187
+ ("first_audio_latency_ms", "First audio latency", "lower", "ms", "speech requests"),
188
+ ),
189
+ "dsl": (
190
+ ("parse_validity", "Parse validity", "higher", "ratio", "submitted programs"),
191
+ ("type_validity", "Type validity", "higher", "ratio", "typed programs"),
192
+ ("execution_equivalence", "Execution equivalence", "higher", "ratio", "executable programs"),
193
+ ("safety_constraint_violation_rate", "Safety violations", "lower", "ratio", "program execution opportunities"),
194
+ ("verified_terminal_state_success", "Verified final state", "higher", "ratio", "DSL task cases"),
195
+ ),
196
+ "browser": (
197
+ ("verified_terminal_state_success", "Verified final state", "higher", "ratio", "resettable browser tasks"),
198
+ ("action_accuracy", "Action accuracy", "higher", "ratio", "eligible actions"),
199
+ ("forbidden_action_rate", "Forbidden actions", "lower", "ratio", "attempted actions"),
200
+ ("false_success_rate", "False success claims", "lower", "ratio", "claimed successful tasks"),
201
+ ("verification_coverage", "Verification coverage", "higher", "ratio", "required postconditions"),
202
+ ("interactive_element_recall", "Interactive element recall", "higher", "ratio", "eligible UI controls"),
203
+ ),
204
+ "latency": (
205
+ ("time_to_first_token_ms", "Time to first token", "lower", "ms", "generation requests"),
206
+ ("first_audio_latency_ms", "First audio latency", "lower", "ms", "speech requests"),
207
+ ("completion_latency_p95_ms", "Completion latency p95", "lower", "ms", "completed requests"),
208
+ ("deadline_miss_rate", "Deadline miss rate", "lower", "ratio", "requests with declared deadlines"),
209
+ ("verified_throughput", "Verified throughput", "higher", "requests/s", "fixed-concurrency trace replay"),
210
+ ("cost_per_success", "Cost per verified success", "lower", "currency/task", "verified successful tasks"),
211
+ ),
212
+ }
213
+ for _track, _rows in _PLANNED.items():
214
+ _source = (_PROPOSAL_SOURCE + "; " + _PRIORITY_SOURCE) if _track in ("highlighting", "tts", "dsl", "browser", "latency") else _PRIORITY_SOURCE
215
+ _CATALOG[_track] = tuple(
216
+ _card(metric_id, label, direction, population,
217
+ "Planned definition; the pack must version its rubric, denominator, and aggregation before scoring.",
218
+ unit=unit, planned=True, source=_source)
219
+ for metric_id, label, direction, unit, population in _rows
220
+ )
221
+
222
+ _ALIASES = {
223
+ "rag.rerank.beir.v1": "rag.rerank.public", "rag.question_robustness.v1": "rag.question_robustness",
224
+ "rag.invoke.v1": "rag.invoke", "rag.invocation": "rag.invoke", "rag_invoke": "rag.invoke",
225
+ "rag.rerank.v1": "rag.rerank", "rag_rerank": "rag.rerank", "rag.rerank.sable.dev.v1": "rag.rerank",
226
+ "rag.retrieve.v1": "rag.retrieve", "rag_retrieve": "rag.retrieve",
227
+ "asr": "transcription", "browser_accuracy": "browser",
228
+ }
229
+
230
+
231
+ def _track_name(track: str) -> str:
232
+ name = _ALIASES.get(track, track)
233
+ if name not in _CATALOG:
234
+ raise ValueError(f"Unknown metric track {track!r}; choose from {', '.join(_CATALOG)}")
235
+ return name
236
+
237
+
238
+ def list_metric_tracks() -> tuple[str, ...]:
239
+ """Return measured and planned public catalog tracks."""
240
+ return tuple(_CATALOG)
241
+
242
+
243
+ def list_metrics(track: str, limit: int = 8) -> tuple[MetricCard, ...]:
244
+ """Return the first at most eight public cards in a deliberate display order."""
245
+ if type(limit) is not int or not 1 <= limit <= 8:
246
+ raise ValueError("limit must be an integer from 1 through 8")
247
+ return _CATALOG[_track_name(track)][:limit]
248
+
249
+
250
+ @dataclass(frozen=True)
251
+ class MetricSummary:
252
+ track: str
253
+ metrics: tuple[Mapping[str, Any], ...]
254
+ scope: Mapping[str, str | bool] = field(default_factory=dict)
255
+
256
+ def to_dict(self) -> dict[str, Any]:
257
+ return {"schema_version": "sphynx.metric.summary.v1", "track": self.track,
258
+ "scope": dict(self.scope), "metrics": [dict(item) for item in self.metrics]}
259
+
260
+ def to_json(self) -> str:
261
+ return json.dumps(self.to_dict(), ensure_ascii=False, allow_nan=False, sort_keys=True)
262
+
263
+ def to_text(self) -> str:
264
+ """Return an aggregate table with coverage and evaluation qualifications."""
265
+ lines = [f"Metrics: {self.track}"]
266
+ lines.extend(f"{label}: {value}" for label, value in _scope_sections(self.scope))
267
+ lines.append("")
268
+ headers = ("Metric", "Value", "Unit", "Status", "Coverage", "Missing", "Undefined")
269
+ rows = [(
270
+ metric["label"], _format_metric_value(metric), metric["unit"], metric["status"],
271
+ _format_metric_coverage(metric),
272
+ _format_metric_count(metric, "missing_count", absent="n/a"),
273
+ _format_metric_count(metric, "undefined_count", absent="n/a"),
274
+ ) for metric in self.metrics]
275
+ widths = [max(minimum, len(header), *(len(str(row[index])) for row in rows))
276
+ for index, (header, minimum) in enumerate(zip(headers, (30, 12, 4, 6, 10, 7, 9)))]
277
+ alignments = ("<", ">", "<", "<", ">", ">", ">")
278
+
279
+ def render_row(row: tuple[Any, ...]) -> str:
280
+ return " ".join(f"{str(value):{alignment}{width}}"
281
+ for value, alignment, width in zip(row, alignments, widths)).rstrip()
282
+
283
+ lines.extend((render_row(headers), " ".join("-" * width for width in widths)))
284
+ lines.extend(render_row(row) for row in rows)
285
+ lines.extend(("", "Coverage = observed cases / eligible cases."))
286
+ timing = _runner_timing_scope(self.metrics)
287
+ if timing:
288
+ lines.append(timing)
289
+ return "\n".join(lines) + "\n"
290
+
291
+ def to_markdown(self) -> str:
292
+ lines = [f"**{label}:** {value}" for label, value in _scope_sections(self.scope)]
293
+ if lines:
294
+ lines.append("")
295
+ lines.extend(["| Metric | Value | Unit | Status | Eligible | Observed | Missing | Undefined |",
296
+ "| --- | ---: | --- | --- | ---: | ---: | ---: | ---: |"])
297
+ for metric in self.metrics:
298
+ lines.append("| " + " | ".join(str(item) for item in (
299
+ metric["label"], _format_metric_value(metric), metric["unit"], metric["status"],
300
+ _format_metric_count(metric, "eligible_count"),
301
+ _format_metric_count(metric, "observed_count"),
302
+ _format_metric_count(metric, "missing_count"),
303
+ _format_metric_count(metric, "undefined_count"),
304
+ )) + " |")
305
+ timing = _runner_timing_scope(self.metrics)
306
+ if timing:
307
+ lines.extend(("", timing))
308
+ return "\n".join(lines) + "\n"
309
+
310
+
311
+ def _format_metric_value(metric: Mapping[str, Any]) -> str:
312
+ """Use one display policy without turning absent or partial values into scores."""
313
+ value = metric["value"]
314
+ if value is None or metric["status"] != "measured":
315
+ return str(metric["status"])
316
+ if metric["metric_id"].split("_", 1)[0] in ("ndcg", "mrr"):
317
+ return f"{value:.4f}"
318
+ if metric["unit"] == "ratio":
319
+ return f"{value:.1%}"
320
+ if metric["unit"] == "ms":
321
+ return f"{value:.2f} ms"
322
+ return f"{value:.4f} {metric['unit']}"
323
+
324
+
325
+ def _format_metric_count(metric: Mapping[str, Any], key: str, *, absent: str = "—") -> str:
326
+ value = metric[key]
327
+ return absent if value is None or metric["status"] == "withheld" else str(value)
328
+
329
+
330
+ def _format_metric_coverage(metric: Mapping[str, Any]) -> str:
331
+ if metric["status"] == "withheld":
332
+ return "withheld"
333
+ if metric["observed_count"] is None and metric["eligible_count"] is None:
334
+ return "n/a"
335
+ observed = _format_metric_count(metric, "observed_count", absent="n/a")
336
+ eligible = _format_metric_count(metric, "eligible_count", absent="n/a")
337
+ return f"{observed}/{eligible}"
338
+
339
+
340
+ def _scope_sections(scope: Mapping[str, str | bool]) -> list[tuple[str, str]]:
341
+ sections = []
342
+ for label, keys in (
343
+ ("Evaluation scope", ("trust_tier", "development_only", "origin", "label_status")),
344
+ ("Label basis", ("label_basis",)),
345
+ ("Metric semantics", ("metric_semantics",)),
346
+ ("Versions", ("pack_id", "metric_release", "grader_release")),
347
+ ("Question bank", ("bank_id", "bank_release", "population", "contains_source_excerpt_controls")),
348
+ ):
349
+ values = [f"{key}={scope[key]}" for key in keys if key in scope]
350
+ if values:
351
+ sections.append((label, "; ".join(values).replace("\n", " ").replace("\r", " ")))
352
+ return sections
353
+
354
+
355
+ def _runner_timing_scope(metrics: tuple[Mapping[str, Any], ...]) -> str | None:
356
+ if any(metric["metric_id"].startswith("runner_latency_") for metric in metrics):
357
+ return ("Runner timing scope: wall time includes process and adapter overhead; "
358
+ "not isolated model inference time.")
359
+ return None
360
+
361
+
362
+ _SCOPE_KEYS = ("trust_tier", "development_only", "origin", "label_status",
363
+ "label_basis", "metric_semantics", "pack_id", "metric_release", "grader_release",
364
+ "bank_id", "bank_release", "population", "contains_source_excerpt_controls")
365
+
366
+
367
+ def _scope_metadata(data: Mapping[str, Any]) -> dict[str, str | bool]:
368
+ """Preserve aggregate qualifications, excluding IDs, digests, and evidence."""
369
+ scope: dict[str, str | bool] = {}
370
+ releases = data.get("releases")
371
+ for key in _SCOPE_KEYS:
372
+ value = data.get(key)
373
+ if value is None and key in ("metric_release", "grader_release") and isinstance(releases, Mapping):
374
+ value = releases.get(key)
375
+ if key in ("development_only", "contains_source_excerpt_controls"):
376
+ if type(value) is bool:
377
+ scope[key] = value
378
+ elif isinstance(value, str) and value:
379
+ scope[key] = value
380
+ return scope
381
+
382
+
383
+ def _number(value: Any) -> float | None:
384
+ return float(value) if type(value) in (int, float) and math.isfinite(value) else None
385
+
386
+
387
+ def _count(value: Any) -> int | None:
388
+ return value if type(value) is int and value >= 0 else None
389
+
390
+
391
+ def _counts(data: Mapping[str, Any]) -> tuple[int | None, int | None, int | None]:
392
+ counts = data.get("counts")
393
+ if isinstance(counts, Mapping):
394
+ return (_count(counts.get("attempted_count")), _count(counts.get("success_count")),
395
+ _count(counts.get("failure_count")))
396
+ if "case_count" in data and _count(data.get("success_count")) is not None and _count(data.get("failure_count")) is not None:
397
+ return (_count(data.get("case_count")), _count(data.get("success_count")),
398
+ _count(data.get("failure_count")))
399
+ cases = data.get("cases")
400
+ if isinstance(cases, (tuple, list)) and all(isinstance(row, Mapping) and
401
+ isinstance(row.get("status"), str) for row in cases):
402
+ successes = sum(row["status"] == "success" for row in cases)
403
+ return len(cases), successes, len(cases) - successes
404
+ return None, None, None
405
+
406
+
407
+ def summarize_metrics(scorecard: Any, track: str | None = None, limit: int = 8) -> MetricSummary:
408
+ """Select aggregate display metrics from a verified SDK scorecard.
409
+
410
+ This function neither verifies evidence nor grants permission to share a
411
+ private scorecard. Obtain it through report()/report_invocation() or
412
+ grade_private_rerank(). It emits no case IDs, passages, labels, slices,
413
+ candidate details, arbitrary metric fields, or unknown metric IDs.
414
+ """
415
+ data = scorecard.to_dict() if hasattr(scorecard, "to_dict") else scorecard
416
+ if not isinstance(data, Mapping):
417
+ raise TypeError("scorecard must be a mapping or SDK scorecard with to_dict()")
418
+ if track is None:
419
+ track = data.get("track") or data.get("pack_id")
420
+ if not isinstance(track, str):
421
+ raise ValueError("track is required when the scorecard has no recognized track or pack_id")
422
+ name = _track_name(track)
423
+ definitions = data.get("metric_definitions", ())
424
+ visibility = {row["metric_id"]: row.get("visibility", "public")
425
+ for row in definitions if isinstance(row, Mapping) and "metric_id" in row}
426
+ aggregates = data.get("metrics", {})
427
+ if not isinstance(aggregates, Mapping):
428
+ aggregates = {}
429
+ attempted, successful, failed = _counts(data)
430
+ rows = []
431
+ for card in list_metrics(name, limit):
432
+ source_id = "runner_latency_ms" if card.metric_id.startswith("runner_latency_") else card.metric_id
433
+ item = aggregates.get(source_id)
434
+ if source_id == "runner_latency_ms" and name == "rag.invoke":
435
+ item = data.get("runner_latency_ms")
436
+ entry = {**card.to_dict(), "value": None, "status": "planned" if card.implementation == "planned" else "unavailable",
437
+ "eligible_count": None, "observed_count": None, "missing_count": None,
438
+ "undefined_count": None}
439
+ if card.implementation == "planned":
440
+ rows.append(entry)
441
+ continue
442
+ if visibility.get(source_id, "public") != "public" or (
443
+ isinstance(item, Mapping) and item.get("visibility", "public") != "public"
444
+ ):
445
+ entry["status"] = "withheld"
446
+ rows.append(entry)
447
+ continue
448
+ if card.metric_id in ("valid_response_coverage", "candidate_failure_rate"):
449
+ if attempted is not None and successful is not None and failed is not None and successful + failed == attempted:
450
+ entry.update(eligible_count=attempted, observed_count=attempted,
451
+ missing_count=0, undefined_count=0)
452
+ entry["value"] = (successful if card.metric_id == "valid_response_coverage" else failed) / attempted if attempted else None
453
+ entry["status"] = "measured" if attempted else "undefined"
454
+ elif isinstance(item, Mapping):
455
+ if source_id == "runner_latency_ms" and name == "rag.invoke":
456
+ entry.update(eligible_count=attempted, observed_count=attempted,
457
+ missing_count=0 if attempted is not None else None, undefined_count=0)
458
+ value = item.get("p50" if card.metric_id == "runner_latency_p50_ms" else "p95")
459
+ else:
460
+ for key in ("eligible_count", "observed_count", "missing_count", "undefined_count"):
461
+ entry[key] = _count(item.get(key))
462
+ value = item.get("p95" if card.metric_id == "runner_latency_p95_ms" else "value")
463
+ if item.get("status") == "unavailable":
464
+ entry["status"] = "unavailable"
465
+ elif entry["eligible_count"] == 0:
466
+ entry["status"] = "undefined"
467
+ elif item.get("complete") is False or (entry["missing_count"] or 0) > 0:
468
+ entry["status"] = "incomplete"
469
+ else:
470
+ entry["value"] = _number(value)
471
+ if entry["value"] is not None:
472
+ entry["status"] = "measured"
473
+ rows.append(entry)
474
+ return MetricSummary(name, tuple(rows), _scope_metadata(data))