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/pipeline.py ADDED
@@ -0,0 +1,313 @@
1
+ """Capture and grade a frozen N11–N17 pipeline without exposing reference labels.
2
+
3
+ The mathematical core is an imports-only port of sablebench's Level 2 grader.
4
+ Execution is explicitly trusted local or pinned replay, never an OS sandbox.
5
+ Missing labels/failed trials remain unavailable; recorded timings are replay.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from copy import deepcopy
10
+ from collections.abc import Mapping
11
+ import math
12
+ from pathlib import Path
13
+ from time import perf_counter
14
+
15
+ from .contracts import ContractError, canonical_json
16
+ from .integration_contracts import CandidateConfiguration, ComparisonIdentity, TrackCaseCapture, value_digest, require_comparable
17
+ from .dataset_v3 import load_dataset_v3
18
+ from ._track_capture import (byte_digest, strict_json, create_run, write_artifact, finish_run,
19
+ read_artifact, verify_track_run, write_evaluation, verify_track_evaluation)
20
+ from .chunking import _policy_for_source, _select_tasks, _verify_approval
21
+ from ._pipeline_core import STAGES, evaluate_level2_run, validate_controlled_change
22
+
23
+ PACK_ID = "rag.pipeline.v1"
24
+ INTERFACE = "sphynx.rag.pipeline.v1"
25
+ METRIC_RELEASE = "rag.pipeline.source-level2.metric.v1"
26
+ GRADER_RELEASE = "rag.pipeline.source-level2.grader.v1"
27
+ POLICY_VERSION = "sphynx.rag.pipeline.policy.v1"
28
+ _CANDIDATE_FIELDS = {"candidate_id","dense_rank","dense_score","reranker_logit","reranker_rank",
29
+ "fusion_score","fusion_rank","dedup_decision","post_dedup_rank","gate_decision","gate_threshold","final_rank"}
30
+
31
+
32
+ def _require(ok, message):
33
+ if not ok: raise ContractError(message)
34
+
35
+
36
+ def _detach(value):
37
+ if isinstance(value,Mapping):
38
+ _require(all(type(key) is str for key in value),"JSON keys must be strings")
39
+ value={key:_detach(child) for key,child in value.items()}
40
+ elif isinstance(value,(list,tuple)):
41
+ value=[_detach(child) for child in value]
42
+ return strict_json(canonical_json(value))
43
+
44
+
45
+ def _number(value):
46
+ try:return type(value) in (int,float) and math.isfinite(value)
47
+ except OverflowError:return False
48
+
49
+
50
+ def build_pipeline_request(task, policy):
51
+ """The request contains input texts/identities, never qrels or evidence truth."""
52
+ pool = [{key:row[key] for key in ("candidate_id","input","input_sha256","dense_rank","dense_score")}
53
+ for row in task["candidates"]]
54
+ body = {"schema_version":"sphynx.rag.pipeline.request.v1", "case_id":task["task_id"],
55
+ "customer_id":task["customer_id"], "query":task["query"],
56
+ "conversation_context":task["conversation_context"], "candidates":pool, "policy":policy}
57
+ if policy.get("boundary")=="composed_source_documents":
58
+ from .chunking import _documents
59
+ body["documents"]=_documents(task)
60
+ body["input_digest"] = value_digest(body)
61
+ return _detach(body)
62
+
63
+
64
+ def validate_pipeline_response(response, request):
65
+ fields={"schema_version","case_id","input_digest","construction","embedding","candidates","latency_state","latency_ms"}
66
+ composed=request["policy"].get("boundary")=="composed_source_documents"
67
+ if composed:fields.add("stages")
68
+ _require(type(response) is dict and set(response)==fields, "pipeline response fields differ")
69
+ _require(response["schema_version"]==("sphynx.rag.pipeline.composed.response.v1" if composed else "sphynx.rag.pipeline.response.v1")
70
+ and response["case_id"]==request["case_id"] and response["input_digest"]==request["input_digest"],
71
+ "pipeline response does not bind its request")
72
+ _require(type(response["construction"]) is dict and type(response["embedding"]) is dict,
73
+ "pipeline construction/embedding traces are required")
74
+ candidates=response["candidates"]
75
+ _require(type(candidates) is list and all(type(row) is dict and set(row)==_CANDIDATE_FIELDS for row in candidates),
76
+ "pipeline must return the exact label-free candidate trace")
77
+ if composed:
78
+ from ._composed_pipeline import validate_chunks,compose_pipeline
79
+ expected=validate_chunks(response["construction"].get("chunks"),request["documents"])
80
+ _require(type(response["stages"]) is dict and set(response["stages"])=={"chunker","embedder","retriever","reranker","fusion","dedup","gate"},"composed stages are incomplete")
81
+ # Reconstruct only the retained transformation, never execute adapters.
82
+ derived=compose_pipeline(request,lambda kind,operation,payload:deepcopy(response["stages"][kind]))
83
+ derived["latency_state"]=response["latency_state"]
84
+ _require(_detach(derived)==response,"composed stage outputs differ from the retained final trace")
85
+ else:expected={row["candidate_id"]:row for row in request["candidates"]}
86
+ ids=[row["candidate_id"] for row in candidates]
87
+ _require(len(ids)==len(set(ids)) and set(ids)==set(expected),"pipeline candidate membership changed")
88
+ for row in candidates:
89
+ source=expected[row["candidate_id"]]
90
+ _require(composed or row["dense_rank"]==source["dense_rank"] and row["dense_score"]==source["dense_score"],
91
+ "pipeline N11 dense input changed")
92
+ for key in ("dense_score","reranker_logit","fusion_score","gate_threshold"):
93
+ _require(_number(row[key]),"pipeline score must be finite")
94
+ for key in ("dense_rank","reranker_rank","fusion_rank"):
95
+ _require(type(row[key]) is int and row[key]>=1,"pipeline ranks must be positive integers")
96
+ for key in ("post_dedup_rank","final_rank"):
97
+ _require(row[key] is None or type(row[key]) is int and row[key]>=1,"pipeline optional ranks differ")
98
+ _require(row["dedup_decision"] in ("keep","drop") and row["gate_decision"] in ("pass","reject"),
99
+ "pipeline decisions differ")
100
+ if row["dedup_decision"]=="drop":
101
+ _require(row["post_dedup_rank"] is None and row["gate_decision"]=="reject" and row["final_rank"] is None,
102
+ "deduplicated candidates cannot reenter later stages")
103
+ else:
104
+ _require(row["post_dedup_rank"] is not None,"kept candidates require post-dedup ranks")
105
+ _require(row["final_rank"] is None or row["gate_decision"]=="pass", "gated candidates cannot reenter N17")
106
+ _require(response["latency_state"] in ("cold","warm"),"pipeline latency state differs")
107
+ _require(type(response["latency_ms"]) is dict and set(response["latency_ms"])<=
108
+ {"retrieval","reranker","fusion","dedup","gate","total","chunking","embedding"},"pipeline timing fields differ")
109
+ _require(all(_number(value) and value>=0 for value in response["latency_ms"].values()),"pipeline timings must be finite/nonnegative")
110
+ # Structural core validation uses neutral labels only; real labels enter later.
111
+ neutral={**deepcopy(response),"query_id":request["case_id"],"answerable":False,
112
+ "input_contract":{"candidate_count":len(candidates),"identity_sha256":request["input_digest"]}}
113
+ neutral["candidates"]=[{**row,"qrel":0,"evidence_group_ids":[]} for row in candidates]
114
+ evaluate_level2_run({"controlled_config":request["policy"]["controlled_config"],"queries":[neutral]})
115
+ kept=sorted((row for row in candidates if row["dedup_decision"]=="keep"),key=lambda row:row["post_dedup_rank"])
116
+ _require([row["post_dedup_rank"] for row in kept]==list(range(1,len(kept)+1)),"post-dedup ranks are not contiguous")
117
+ return _detach(response)
118
+
119
+
120
+ def _policy(dataset, source_digest, candidate, *, boundary="frozen_n11", **options):
121
+ _require(boundary in ("frozen_n11","composed_source_documents"),"pipeline execution boundary differs")
122
+ policy=_policy_for_source(dataset,source_digest,**options)
123
+ controlled=candidate.parameters.get("controlled_config")
124
+ _require(isinstance(controlled,Mapping) and type(controlled.get("top_k")) is int
125
+ and 1<=controlled["top_k"]<=50,"candidate needs a bounded controlled_config.top_k")
126
+ policy.update(version=POLICY_VERSION,controlled_config=_detach(controlled),
127
+ boundary=boundary,
128
+ execution_mode=candidate.execution_mode,timing_authority="component_reported_unattested",
129
+ missing_label_policy="unavailable_not_zero")
130
+ return policy
131
+
132
+
133
+ def _identity(dataset, digest, tasks, policy):
134
+ # Controlled component values are captured separately. The comparison gate
135
+ # explicitly verifies every changed path rather than hiding them in one key.
136
+ fixed={key:value for key,value in policy.items() if key!="controlled_config"}
137
+ return ComparisonIdentity("rag_pipeline",PACK_ID,"v1","sable.rag.dataset.v3",digest,
138
+ value_digest([task["task_id"] for task in tasks]),
139
+ value_digest([{key:task[key] for key in ("task_id","answerability","candidates","evidence_groups")} for task in tasks]),
140
+ value_digest(fixed),METRIC_RELEASE,GRADER_RELEASE,
141
+ policy["boundary_digest"] or value_digest({"boundary":"N11-N17", "inputs":digest}))
142
+
143
+
144
+ def run_pipeline(candidate, adapter, dataset_path, output_dir, *, expected_sha256,
145
+ synthetic=False, owner_authorized=False, pack_manifest_path=None,
146
+ trials=1, attempts=1, case_ids=None, boundary_digest=None,boundary="frozen_n11"):
147
+ _require(isinstance(candidate,CandidateConfiguration) and candidate.interface_version==INTERFACE,
148
+ "pipeline candidate interface differs")
149
+ _require(candidate.execution_mode in ("trusted_local","recorded"),"pipeline execution mode requires an explicit adapter")
150
+ dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
151
+ policy=_policy(dataset,expected_sha256,candidate,synthetic=synthetic,owner_authorized=owner_authorized,
152
+ pack_manifest_path=pack_manifest_path,trials=trials,attempts=attempts,boundary_digest=boundary_digest,boundary=boundary)
153
+ tasks=_select_tasks(dataset,case_ids)
154
+ if candidate.execution_mode=="recorded":
155
+ _require(isinstance(adapter,Mapping) and candidate.parameters.get("recordings_digest")==value_digest(adapter),
156
+ "pipeline replay requires the complete pinned recording")
157
+ adapter=_detach(adapter)
158
+ else: _require(callable(adapter),"pipeline adapter must be callable")
159
+ observed={"approved_manifest_source":None if synthetic else Path(pack_manifest_path).read_text(encoding="utf-8")}
160
+ _verify_approval(dataset,expected_sha256,policy,observed)
161
+ identity=_identity(dataset,expected_sha256,tasks,policy)
162
+ ids=[task["task_id"] for task in tasks]
163
+ directory=create_run(output_dir,identity,candidate,ids)
164
+ captures=[]
165
+ for index,task in enumerate(tasks):
166
+ request=build_pipeline_request(task,policy)
167
+ for trial in range(1,trials+1):
168
+ for attempt in range(1,attempts+1):
169
+ prefix=f"cases/{index}/{trial}/{attempt}/"
170
+ req=write_artifact(directory,prefix+"request.json",request)
171
+ response=None;failure=None;begin=perf_counter()
172
+ try:
173
+ raw=adapter[value_digest(request)] if candidate.execution_mode=="recorded" else adapter(_detach(request))
174
+ response=write_artifact(directory,prefix+"response.json",raw)
175
+ validate_pipeline_response(raw,request)
176
+ except Exception:
177
+ failure="invalid_response" if response is not None else "candidate_failure"
178
+ observation=write_artifact(directory,prefix+"observed.json",observed)
179
+ captures.append(TrackCaseCapture(task["task_id"],trial,attempt,req,response,observation,failure,
180
+ (perf_counter()-begin)*1000))
181
+ return finish_run(directory,identity,candidate,ids,captures)
182
+
183
+
184
+ def _verified(run,dataset_path,digest):
185
+ dataset=load_dataset_v3(dataset_path,expected_sha256=digest)
186
+ manifest=verify_track_run(run)
187
+ _require(manifest.candidate.interface_version==INTERFACE,"capture is not a pipeline experiment")
188
+ tasks=_select_tasks(dataset,manifest.case_ids)
189
+ policy=read_artifact(run.path,manifest.captures[0].request).get("policy")
190
+ _require(type(policy) is dict and set(policy)=={"version","synthetic","approved_manifest_digest","trials","attempts","selection","boundary_digest",
191
+ "controlled_config","execution_mode","timing_authority","missing_label_policy","boundary"} and policy.get("version")==POLICY_VERSION and
192
+ policy.get("execution_mode")==manifest.candidate.execution_mode,"captured pipeline policy differs")
193
+ _require(type(policy["synthetic"]) is bool and policy["selection"]=="first_success_per_trial"
194
+ and policy["timing_authority"]=="component_reported_unattested" and policy["missing_label_policy"]=="unavailable_not_zero"
195
+ and policy["controlled_config"]==_detach(manifest.candidate.parameters["controlled_config"])
196
+ and policy["boundary"] in ("frozen_n11","composed_source_documents"),"pipeline policy is not the frozen declared policy")
197
+ _require(all(type(policy[key]) is int and policy[key]>=1 for key in ("trials","attempts")),"pipeline trial/attempt policy differs")
198
+ expected=_policy(dataset,digest,manifest.candidate,synthetic=policy.get("synthetic"),
199
+ owner_authorized=True,pack_manifest_path=None,trials=policy.get("trials"),attempts=policy.get("attempts"),
200
+ boundary_digest=policy.get("boundary_digest"),boundary=policy["boundary"]) if policy.get("synthetic") else None
201
+ if expected is not None:_require(policy==expected,"pipeline policy shape or values changed")
202
+ require_comparable(_identity(dataset,digest,tasks,policy),manifest.identity)
203
+ requests={task["task_id"]:build_pipeline_request(task,policy) for task in tasks}
204
+ verify_track_run(run,expected_case_ids=manifest.case_ids,expected_requests=requests,
205
+ trials=policy["trials"],attempts=policy["attempts"])
206
+ for capture in manifest.captures:
207
+ _require(capture.observed is not None,"pipeline approval observation is missing")
208
+ _verify_approval(dataset,digest,policy,read_artifact(run.path,capture.observed))
209
+ return manifest,tasks,policy,requests
210
+
211
+
212
+ def _score(task,response,request):
213
+ response=validate_pipeline_response(response,request)
214
+ labels={row["candidate_id"]:row for row in task["candidates"]}
215
+ if request["policy"]["boundary"]=="composed_source_documents":
216
+ from ._composed_pipeline import validate_chunks,truth_for_chunks
217
+ chunks=validate_chunks(response["construction"]["chunks"],request["documents"])
218
+ labels=truth_for_chunks(task,chunks)
219
+ if labels is None:return None,len(task["candidates"])
220
+ missing=sum(row.get("qrel") is None or row.get("judgment_status")!="judged" for row in labels.values())
221
+ if missing:return None,missing
222
+ query={key:response[key] for key in ("construction","embedding","latency_state","latency_ms")}
223
+ query.update(query_id=task["task_id"],answerable=task["answerability"]=="answerable",
224
+ input_contract={"candidate_count":len(labels),"identity_sha256":request["input_digest"]},
225
+ candidates=[{**row,"qrel":labels[row["candidate_id"]]["qrel"],
226
+ "evidence_group_ids":labels[row["candidate_id"]]["evidence_group_ids"]} for row in response["candidates"]])
227
+ score=evaluate_level2_run({"controlled_config":request["policy"]["controlled_config"],"queries":[query]})
228
+ # Keep the canonical core's diagnostic output, but do not promote its legacy
229
+ # zero-on-empty aggregate into a defined ranking score for an unsupported pool.
230
+ metric_eligibility={"ndcg_at_5":any(row["qrel"]>0 for row in task["candidates"]),
231
+ "recall_at_5":any(row["qrel"]>=2 for row in task["candidates"]),
232
+ "mrr":any(row["qrel"]>=2 for row in task["candidates"])}
233
+ score["metric_eligibility"]=metric_eligibility
234
+ if not all(metric_eligibility.values()):
235
+ score["source_stage_metrics"]=_detach(score["stage_metrics"])
236
+ for stage in STAGES:
237
+ for key,eligible in metric_eligibility.items():
238
+ if not eligible:score["stage_metrics"][stage][key]=None
239
+ score["ranking_status"]="partially_eligible_no_strong_reference" if any(metric_eligibility.values()) else "undefined_no_positive_reference"
240
+ else:score["ranking_status"]="measured"
241
+ return score,0
242
+
243
+
244
+ def _grade_metrics(run,dataset_path,expected_sha256):
245
+ manifest,tasks,policy,requests=_verified(run,dataset_path,expected_sha256)
246
+ rows=[]
247
+ for task in tasks:
248
+ for trial in range(1,policy["trials"]+1):
249
+ score=None;selected=None;missing=0
250
+ for capture in sorted(manifest.captures,key=lambda row:row.attempt_index):
251
+ if capture.case_id!=task["task_id"] or capture.trial_index!=trial or capture.failure is not None:continue
252
+ _require(capture.response is not None,"successful pipeline capture has no response")
253
+ value,absent=_score(task,read_artifact(run.path,capture.response),requests[task["task_id"]])
254
+ if selected is None:score,missing,selected=value,absent,capture
255
+ rows.append({"case_id":task["task_id"],"trial_index":trial,"metrics":score,
256
+ "status":"failed" if selected is None else "labels_missing" if missing else "measured",
257
+ "missing_label_count":missing,"runner_wall_ms":None if selected is None else selected.runner_latency_ms})
258
+ good=[row["metrics"] for row in rows if row["metrics"] is not None]
259
+ def aggregate(stage,key):
260
+ values=[row["stage_metrics"][stage][key] for row in good if row["stage_metrics"][stage][key] is not None]
261
+ return None if len(good)!=len(rows) or not values else sum(values)/len(values)
262
+ stage={stage:{key:aggregate(stage,key) for key in ("candidate_count","recall_at_5","ndcg_at_5","mrr")} for stage in STAGES}
263
+ undefined=sum(row["metrics"] is not None and row["metrics"]["ranking_status"]=="undefined_no_positive_reference" for row in rows)
264
+ undefined_by_metric={key:sum(not row["metric_eligibility"][key] for row in good) for key in ("recall_at_5","ndcg_at_5","mrr")}
265
+ accounting={stage:{key:{"eligible_count":len(rows)-undefined_by_metric.get(key,0),
266
+ "observed_count":sum(row["stage_metrics"][stage][key] is not None for row in good),
267
+ "missing_count":len(rows)-len(good),"undefined_count":undefined_by_metric.get(key,0)}
268
+ for key in ("candidate_count","recall_at_5","ndcg_at_5","mrr")} for stage in STAGES}
269
+ return manifest,{"stage_metrics":stage,"cases":rows,
270
+ "stage_metric_accounting":accounting,"ranking_eligible_count":len(rows)-undefined,
271
+ "ranking_fully_eligible_count":sum(all(row["metric_eligibility"].values()) for row in good),
272
+ "ranking_undefined_no_positive_count":undefined,"quality_complete_meaning":"all_capture_and_reference_rows_available",
273
+ "coverage":sum(row["status"]!="failed" for row in rows)/len(rows),"quality_complete":len(good)==len(rows),
274
+ "quality_observed_count":len(good),"quality_missing_count":len(rows)-len(good),
275
+ "quality_scope":"synthetic_fixture" if policy["synthetic"] else "owner_approved",
276
+ "execution_mode":policy["execution_mode"],"timing_authority":"component_reported_unattested",
277
+ "runner_timing_scope":"replay_adapter_wall" if policy["execution_mode"]=="recorded" else "trusted_local_adapter_wall",
278
+ "controlled_config":policy["controlled_config"],"boundary":policy["boundary"]}
279
+
280
+
281
+ def grade_pipeline(run,dataset_path,*,expected_sha256):
282
+ manifest,metrics=_grade_metrics(run,dataset_path,expected_sha256)
283
+ return write_evaluation(run,manifest.identity,metrics)
284
+
285
+
286
+ def report_pipeline(evaluation,dataset_path,*,expected_sha256):
287
+ _,expected=_grade_metrics(evaluation.run,dataset_path,expected_sha256)
288
+ report=verify_track_evaluation(evaluation).to_dict()
289
+ _require(report["metrics"]==expected,"pipeline evaluation differs from its retained captures/references")
290
+ return report
291
+
292
+
293
+ def compare_pipeline(baseline,candidate,dataset_path,*,expected_sha256,declared_paths=()):
294
+ left=report_pipeline(baseline,dataset_path,expected_sha256=expected_sha256)
295
+ right=report_pipeline(candidate,dataset_path,expected_sha256=expected_sha256)
296
+ return _compare_report_data(left,right,declared_paths=declared_paths)
297
+
298
+
299
+ def _compare_report_data(left,right,*,declared_paths=()):
300
+ """Native comparison arithmetic over already verified reports; no I/O/grant."""
301
+ require_comparable(ComparisonIdentity.from_dict(left["identity"]),ComparisonIdentity.from_dict(right["identity"]))
302
+ a,b=left["metrics"],right["metrics"]
303
+ changed=None
304
+ if a["controlled_config"]!=b["controlled_config"]:
305
+ changed=validate_controlled_change(a["controlled_config"],b["controlled_config"],declared_paths)
306
+ else:_require(not declared_paths,"declared pipeline change did not occur")
307
+ _require([(r["case_id"],r["trial_index"]) for r in a["cases"]]==[(r["case_id"],r["trial_index"]) for r in b["cases"]],"pipeline paired cohort differs")
308
+ delta={stage:{key:None if a["stage_metrics"][stage][key] is None or b["stage_metrics"][stage][key] is None
309
+ else b["stage_metrics"][stage][key]-a["stage_metrics"][stage][key] for key in a["stage_metrics"][stage]} for stage in STAGES}
310
+ return {"schema_version":"sphynx.rag.pipeline.comparison.v1","baseline":left["evaluation_id"],
311
+ "candidate":right["evaluation_id"],"changed_component":changed,"delta":delta,
312
+ "direction":"candidate_minus_baseline","timing_comparable":False,
313
+ "notice":"Component timings are caller-reported diagnostics; local execution is not attested."}