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/catalog.py ADDED
@@ -0,0 +1,272 @@
1
+ """Content-free discovery contracts and independently owned catalog fragments.
2
+
3
+ Discovery imports only descriptor fragments, never adapter constructors. Fragment
4
+ modules contain DATASETS and/or CANDIDATES tuples and import no optional model or
5
+ provider dependency. Readiness describes an approved release; caller-local files,
6
+ models, credentials and permission are still checked by execution preflight.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from importlib import import_module
12
+ from typing import Any
13
+
14
+ from .contracts import ContractError, _finite_number, _id, _text, _tuple_of_ids
15
+ from .integration_contracts import (
16
+ CONFIGURATION_SCHEMA, ScoreField, WireValue, _choice, _count,
17
+ _optional_digest, _schema, _sequence,
18
+ )
19
+
20
+ DATASET_DESCRIPTOR_SCHEMA = "sphynx.dataset-descriptor.v1"
21
+ CANDIDATE_DESCRIPTOR_SCHEMA = "sphynx.candidate-descriptor.v1"
22
+ # Integration ownership is fixed here so workers add their own data-only module.
23
+ CATALOG_PROVIDER_MODULES = (
24
+ "sphynx.dataset_catalog_data", # I-02 dataset release descriptors
25
+ "sphynx.invocation_catalog_data", # I-03 routing candidates
26
+ "sphynx.model_catalog_data", # I-04 optional/recorded model candidates
27
+ "sphynx.learned_catalog_data", # I-05 learned candidates
28
+ "sphynx.statistical_catalog_data", # I-06 statistical/guarded candidates
29
+ "sphynx.retrieval_catalog_data", # I-07 retrieval candidates
30
+ "sphynx.workbench_catalog_data", # I-10/I-11/I-12 via integration owner
31
+ )
32
+
33
+
34
+ def _texts(value: Any, field: str) -> tuple[str, ...]:
35
+ values = _sequence(value, str, field)
36
+ for item in values:
37
+ _text(item, field)
38
+ if len(set(values)) != len(values):
39
+ raise ContractError(f"{field} must contain unique values")
40
+ return values
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class DatasetRelationship(WireValue):
45
+ dataset_id: str
46
+ kind: str
47
+ case_count: int | None = None
48
+ family_digest: str | None = None
49
+
50
+ def __post_init__(self) -> None:
51
+ _id(self.dataset_id, "related dataset_id")
52
+ _choice(self.kind, ("subset_of", "revised_from", "repeated_question", "shared_family", "shared_source", "supersedes"), "relationship kind")
53
+ if self.case_count is not None:
54
+ _count(self.case_count, "relationship case_count")
55
+ _optional_digest(self.family_digest, "relationship family_digest")
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class DatasetDescriptor(WireValue):
60
+ dataset_id: str
61
+ pack_id: str
62
+ release: str
63
+ track: str
64
+ dataset_schema_version: str
65
+ source_digest: str | None
66
+ reference_digest: str | None
67
+ population: str
68
+ split: str
69
+ case_count: int | None
70
+ relevance_threshold: float | None
71
+ review_status: str
72
+ state: str
73
+ reason: str | None = None
74
+ source_case_count: int | None = None
75
+ candidate_depth: int | None = None
76
+ first_stage: str | None = None
77
+ family_status: str = "unknown"
78
+ family_digest: str | None = None
79
+ repeated_question_status: str = "unknown"
80
+ relationships: tuple[DatasetRelationship, ...] = ()
81
+ authorization_requirements: tuple[str, ...] = ()
82
+ local_prerequisites: tuple[str, ...] = ()
83
+ implementation_status: str = "pending"
84
+ previously_exercised: bool | None = None
85
+ superseded_by: tuple[str, ...] = ()
86
+ schema_version: str = DATASET_DESCRIPTOR_SCHEMA
87
+
88
+ def __post_init__(self) -> None:
89
+ _schema(self.schema_version, DATASET_DESCRIPTOR_SCHEMA)
90
+ for name in ("dataset_id", "pack_id", "release", "track", "dataset_schema_version", "population", "split", "review_status"):
91
+ _id(getattr(self, name), name)
92
+ for name in ("source_digest", "reference_digest", "family_digest"):
93
+ _optional_digest(getattr(self, name), name)
94
+ for name in ("case_count", "source_case_count", "candidate_depth"):
95
+ if getattr(self, name) is not None:
96
+ _count(getattr(self, name), name, 1 if name == "candidate_depth" else 0)
97
+ if self.source_case_count is not None and self.case_count is not None and self.source_case_count < self.case_count:
98
+ raise ContractError("source_case_count must include the selected cohort")
99
+ if self.relevance_threshold is not None:
100
+ object.__setattr__(self, "relevance_threshold", _finite_number(self.relevance_threshold, "relevance_threshold"))
101
+ if self.first_stage is not None:
102
+ _id(self.first_stage, "first_stage")
103
+ _choice(self.state, ("ready", "blocked", "superseded"), "dataset state")
104
+ _choice(self.implementation_status, ("available", "pending", "external_import_required"), "implementation_status")
105
+ _choice(self.family_status, ("known", "unknown", "not_applicable"), "family_status")
106
+ _choice(self.repeated_question_status, ("known", "unknown", "not_applicable"), "repeated_question_status")
107
+ if self.state == "ready" and (self.source_digest is None or self.reference_digest is None or self.implementation_status != "available"):
108
+ raise ContractError("ready requires exact source/reference identity and an available loader")
109
+ if self.state != "ready" and self.reason is None:
110
+ raise ContractError("blocked or superseded datasets require an exact reason")
111
+ if self.reason is not None:
112
+ _text(self.reason, "reason")
113
+ if self.family_status == "known" and self.family_digest is None:
114
+ raise ContractError("known family relationships require a family digest")
115
+ relationships = _sequence(self.relationships, DatasetRelationship, "relationships")
116
+ if any(item.dataset_id == self.dataset_id for item in relationships):
117
+ raise ContractError("dataset relationships must not reference themselves")
118
+ keys = [(item.dataset_id, item.kind) for item in relationships]
119
+ if len(set(keys)) != len(keys):
120
+ raise ContractError("dataset relationships must be unique")
121
+ object.__setattr__(self, "relationships", relationships)
122
+ for name in ("authorization_requirements", "local_prerequisites"):
123
+ object.__setattr__(self, name, _texts(getattr(self, name), name))
124
+ object.__setattr__(self, "superseded_by", _tuple_of_ids(self.superseded_by, "superseded_by"))
125
+ if self.state == "superseded" and not self.superseded_by:
126
+ raise ContractError("superseded state requires distinct replacement dataset IDs")
127
+ if self.dataset_id in self.superseded_by or (self.state != "superseded" and self.superseded_by):
128
+ raise ContractError("invalid superseded_by identity")
129
+ if self.previously_exercised is not None and type(self.previously_exercised) is not bool:
130
+ raise ContractError("previously_exercised must be boolean or unknown/null")
131
+
132
+ @classmethod
133
+ def from_dict(cls, value):
134
+ from collections.abc import Mapping
135
+ checked = cls._checked_fields(value)
136
+ if isinstance(checked, dict) and "relationships" in checked:
137
+ checked["relationships"] = tuple(DatasetRelationship.from_dict(item) for item in _sequence(checked["relationships"], Mapping, "relationships"))
138
+ return super().from_dict(checked)
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class CandidateDescriptor(WireValue):
143
+ candidate_id: str
144
+ track: str
145
+ interface_version: str
146
+ source_digest: str | None
147
+ supported_dataset_ids: tuple[str, ...]
148
+ execution_modes: tuple[str, ...]
149
+ implementation_status: str
150
+ reason: str | None = None
151
+ local_prerequisites: tuple[str, ...] = ()
152
+ dependencies: tuple[str, ...] = ()
153
+ score_fields: tuple[ScoreField, ...] = ()
154
+ model_identity_required: bool = False
155
+ supported_python: tuple[str, ...] = ()
156
+ configuration_schema_version: str = CONFIGURATION_SCHEMA
157
+ schema_version: str = CANDIDATE_DESCRIPTOR_SCHEMA
158
+
159
+ def __post_init__(self) -> None:
160
+ _schema(self.schema_version, CANDIDATE_DESCRIPTOR_SCHEMA)
161
+ _schema(self.configuration_schema_version, CONFIGURATION_SCHEMA)
162
+ for name in ("candidate_id", "track", "interface_version"):
163
+ _id(getattr(self, name), name)
164
+ _optional_digest(self.source_digest, "source_digest")
165
+ _choice(self.implementation_status, ("available", "pending", "blocked"), "implementation_status")
166
+ if self.implementation_status == "available" and self.source_digest is None:
167
+ raise ContractError("available candidates require a source identity")
168
+ if self.implementation_status != "available" and self.reason is None:
169
+ raise ContractError("pending/blocked candidates require a reason")
170
+ if self.reason is not None:
171
+ _text(self.reason, "reason")
172
+ object.__setattr__(self, "supported_dataset_ids", _tuple_of_ids(self.supported_dataset_ids, "supported_dataset_ids"))
173
+ modes = _texts(self.execution_modes, "execution_modes")
174
+ if not modes:
175
+ raise ContractError("execution_modes must be nonempty")
176
+ for mode in modes:
177
+ _choice(mode, ("recorded", "trusted_local", "external_command", "live_provider"), "execution mode")
178
+ object.__setattr__(self, "execution_modes", modes)
179
+ for name in ("local_prerequisites", "dependencies", "supported_python"):
180
+ object.__setattr__(self, name, _texts(getattr(self, name), name))
181
+ scores = _sequence(self.score_fields, ScoreField, "score_fields")
182
+ if len({item.field_id for item in scores}) != len(scores):
183
+ raise ContractError("candidate score field identities must be unique")
184
+ object.__setattr__(self, "score_fields", scores)
185
+ if type(self.model_identity_required) is not bool:
186
+ raise ContractError("model_identity_required must be a boolean")
187
+
188
+ @classmethod
189
+ def from_dict(cls, value):
190
+ from collections.abc import Mapping
191
+ checked = cls._checked_fields(value)
192
+ if isinstance(checked, dict) and "score_fields" in checked:
193
+ checked["score_fields"] = tuple(ScoreField.from_dict(item) for item in _sequence(checked["score_fields"], Mapping, "score_fields"))
194
+ return super().from_dict(checked)
195
+
196
+
197
+ def merge_catalog_descriptors(*groups):
198
+ """Merge data-only fragments; no ID may overwrite a previous release."""
199
+ result = []
200
+ seen = set()
201
+ for group in groups:
202
+ if not isinstance(group, (tuple, list)):
203
+ raise ContractError("catalog fragments must be descriptor sequences")
204
+ for item in group:
205
+ if isinstance(item, DatasetDescriptor):
206
+ key = ("dataset", item.dataset_id)
207
+ elif isinstance(item, CandidateDescriptor):
208
+ key = ("candidate", item.candidate_id)
209
+ else:
210
+ raise ContractError("catalog fragments must contain validated descriptors")
211
+ if key in seen:
212
+ raise ContractError(f"catalog identity {key[1]!r} is duplicated or substitutes a release")
213
+ seen.add(key)
214
+ result.append(item)
215
+ return tuple(result)
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class Catalog:
220
+ datasets: tuple[DatasetDescriptor, ...] = ()
221
+ candidates: tuple[CandidateDescriptor, ...] = ()
222
+
223
+ def __post_init__(self) -> None:
224
+ datasets = _sequence(self.datasets, DatasetDescriptor, "datasets")
225
+ candidates = _sequence(self.candidates, CandidateDescriptor, "candidates")
226
+ merge_catalog_descriptors(datasets, candidates)
227
+ object.__setattr__(self, "datasets", datasets)
228
+ object.__setattr__(self, "candidates", candidates)
229
+
230
+ def list_datasets(self, track: str | None = None) -> tuple[DatasetDescriptor, ...]:
231
+ return tuple(item for item in self.datasets if track is None or item.track == track)
232
+
233
+ def get_dataset(self, dataset_id: str) -> DatasetDescriptor:
234
+ _id(dataset_id, "dataset_id")
235
+ for item in self.datasets:
236
+ if item.dataset_id == dataset_id:
237
+ return item
238
+ raise ValueError(f"Unknown dataset {dataset_id!r}; call list_datasets() for installed release IDs")
239
+
240
+ def list_candidates(self, track: str | None = None) -> tuple[CandidateDescriptor, ...]:
241
+ return tuple(item for item in self.candidates if track is None or item.track == track)
242
+
243
+
244
+ def _installed_catalog() -> Catalog:
245
+ groups = []
246
+ for name in CATALOG_PROVIDER_MODULES:
247
+ try:
248
+ module = import_module(name)
249
+ except ModuleNotFoundError as exc:
250
+ if exc.name != name:
251
+ raise # An installed provider with a missing dependency must fail visibly.
252
+ continue
253
+ groups.append(getattr(module, "DATASETS", ()))
254
+ groups.append(getattr(module, "CANDIDATES", ()))
255
+ descriptors = merge_catalog_descriptors(*groups)
256
+ return Catalog(
257
+ tuple(item for item in descriptors if isinstance(item, DatasetDescriptor)),
258
+ tuple(item for item in descriptors if isinstance(item, CandidateDescriptor)),
259
+ )
260
+
261
+
262
+ def list_datasets(track: str | None = None) -> tuple[DatasetDescriptor, ...]:
263
+ """List installed content-free releases, without data/model/provider execution."""
264
+ return _installed_catalog().list_datasets(track)
265
+
266
+
267
+ def get_dataset(dataset_id: str) -> DatasetDescriptor:
268
+ return _installed_catalog().get_dataset(dataset_id)
269
+
270
+
271
+ def list_candidates(track: str | None = None) -> tuple[CandidateDescriptor, ...]:
272
+ return _installed_catalog().list_candidates(track)
sphynx/chunking.py ADDED
@@ -0,0 +1,372 @@
1
+ """Sphynx-owned port of the source contracts; no sibling runtime dependency."""
2
+ from __future__ import annotations
3
+ from typing import Any
4
+ JsonValue = Any
5
+
6
+ import hashlib, math, re
7
+ from dataclasses import dataclass
8
+ from collections.abc import Mapping, Sequence
9
+ from typing import cast
10
+ DEPTHS = (5, 10, 15, 30)
11
+ class Level1Error(ValueError):
12
+ """Raised when a Level 1 evaluation is not causally comparable."""
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class Chunk:
16
+ chunk_id: str
17
+ document_id: str
18
+ start: int
19
+ end: int
20
+ text: str
21
+
22
+ def _percentile(values: Sequence[float], fraction: float) -> float | None:
23
+ if not values:
24
+ return None
25
+ ordered = sorted(values)
26
+ position = (len(ordered) - 1) * fraction
27
+ lower = math.floor(position)
28
+ upper = math.ceil(position)
29
+ if lower == upper:
30
+ return ordered[lower]
31
+ return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
32
+
33
+ def _distribution(values: Sequence[float]) -> dict[str, float | int | None]:
34
+ return {'count': len(values), 'min': min(values) if values else None, 'mean': sum(values) / len(values) if values else None, 'p50': _percentile(values, 0.5), 'p95': _percentile(values, 0.95), 'max': max(values) if values else None}
35
+
36
+ def _token_spans(text: str) -> tuple[tuple[int, int], ...]:
37
+ return tuple(((match.start(), match.end()) for match in re.finditer('\\S+', text)))
38
+
39
+ def evaluate_chunking(documents: Mapping[str, str], source_spans: Sequence[Mapping[str, object]], evidence_groups: Sequence[Mapping[str, object]], chunks: Sequence[Chunk], *, expected_document_sha256: Mapping[str, str]) -> dict[str, JsonValue]:
40
+ """Score a versioned chunk set against immutable source spans."""
41
+ for document_id, content in documents.items():
42
+ digest = hashlib.sha256(content.encode()).hexdigest()
43
+ if expected_document_sha256.get(document_id) != digest:
44
+ raise Level1Error(f'document {document_id} does not match its immutable hash')
45
+ by_document: dict[str, list[Chunk]] = {document_id: [] for document_id in documents}
46
+ for chunk in chunks:
47
+ document_content = documents.get(chunk.document_id)
48
+ if document_content is None or chunk.start < 0 or chunk.end <= chunk.start or (chunk.end > len(document_content)):
49
+ raise Level1Error(f'chunk {chunk.chunk_id} has invalid document bounds')
50
+ if chunk.text != document_content[chunk.start:chunk.end]:
51
+ raise Level1Error(f'chunk {chunk.chunk_id} text does not bind its source document')
52
+ by_document[chunk.document_id].append(chunk)
53
+ contained = 0
54
+ intersections: list[int] = []
55
+ gold_token_total = 0
56
+ covered_gold_tokens = 0
57
+ emitted_token_total = 0
58
+ relevant_emitted_tokens = 0
59
+ duplicate_positions = 0
60
+ relevant_chunk_ids: set[str] = set()
61
+ span_to_chunks: dict[str, set[str]] = {}
62
+ for span in source_spans:
63
+ document_id = cast('str', span['document_id'])
64
+ start, end = (cast('int', span['start']), cast('int', span['end']))
65
+ span_id = cast('str', span['source_span_id'])
66
+ overlapping = {chunk.chunk_id for chunk in by_document.get(document_id, []) if max(start, chunk.start) < min(end, chunk.end)}
67
+ intersections.append(len(overlapping))
68
+ span_to_chunks[span_id] = overlapping
69
+ relevant_chunk_ids.update(overlapping)
70
+ contained += int(any((chunk.start <= start and chunk.end >= end for chunk in by_document.get(document_id, []))))
71
+ tokens = _token_spans(documents[document_id])
72
+ gold_positions = {(token_start, token_end) for token_start, token_end in tokens if token_start < end and token_end > start}
73
+ gold_token_total += len(gold_positions)
74
+ coverage: dict[tuple[int, int], int] = {position: 0 for position in gold_positions}
75
+ for chunk in by_document.get(document_id, []):
76
+ for position in gold_positions:
77
+ if position[0] < chunk.end and position[1] > chunk.start:
78
+ coverage[position] += 1
79
+ covered_gold_tokens += sum((count > 0 for count in coverage.values()))
80
+ for document_id, document_chunks in by_document.items():
81
+ positions: dict[tuple[int, int], int] = {}
82
+ relevant_ranges = [(cast('int', span['start']), cast('int', span['end'])) for span in source_spans if span['document_id'] == document_id]
83
+ for chunk in document_chunks:
84
+ for position in _token_spans(documents[document_id]):
85
+ if position[0] < chunk.end and position[1] > chunk.start:
86
+ emitted_token_total += 1
87
+ positions[position] = positions.get(position, 0) + 1
88
+ relevant_emitted_tokens += int(any((position[0] < end and position[1] > start for start, end in relevant_ranges)))
89
+ duplicate_positions += sum((max(count - 1, 0) for count in positions.values()))
90
+ required_groups = [group for group in evidence_groups if group['requirement'] == 'required']
91
+ oracle_order = sorted(chunks, key=lambda chunk: (-sum((len(span_to_chunks.get(cast('str', span_id), set()) & {chunk.chunk_id}) * cast('float', group['weight']) for group in evidence_groups for span_id in cast('list[object]', group['source_span_ids']))), chunk.chunk_id))
92
+ group_recall: dict[str, float | None] = {}
93
+ for depth in DEPTHS:
94
+ selected = {chunk.chunk_id for chunk in oracle_order[:depth]}
95
+ covered = sum((any((selected & span_to_chunks.get(cast('str', span_id), set()) for span_id in cast('list[object]', group['source_span_ids']))) for group in required_groups))
96
+ group_recall[str(depth)] = None if not required_groups else covered / len(required_groups)
97
+ chunk_chars = [float(chunk.end - chunk.start) for chunk in chunks]
98
+ chunk_tokens = [float(len(_token_spans(chunk.text))) for chunk in chunks]
99
+ return cast('dict[str, JsonValue]', {'evidence_span_containment': None if not source_spans else contained / len(source_spans), 'boundary_fragmentation': _distribution([float(value) for value in intersections]), 'gold_token_precision': None if emitted_token_total == 0 else relevant_emitted_tokens / emitted_token_total, 'gold_token_recall': None if gold_token_total == 0 else covered_gold_tokens / gold_token_total, 'evidence_density': None if not chunks else len(relevant_chunk_ids) / len(chunks), 'duplicate_token_rate': None if emitted_token_total == 0 else duplicate_positions / emitted_token_total, 'chunk_character_distribution': _distribution(chunk_chars), 'chunk_token_distribution': _distribution(chunk_tokens), 'oracle_evidence_group_recall': group_recall})
100
+
101
+ # Standalone Sphynx execution surface. Only captured responses enter grading.
102
+ from dataclasses import asdict, replace
103
+ from time import perf_counter
104
+ from .contracts import ContractError
105
+ from .integration_contracts import CandidateConfiguration, ComparisonIdentity, TrackCaseCapture, value_digest, require_comparable
106
+ from .dataset_v3 import load_dataset_v3, assess_readiness, intake_validate_pack_manifest, scan_privacy
107
+ from ._track_capture import (create_run, write_artifact, finish_run, read_artifact, verify_track_run,
108
+ write_evaluation, verify_track_evaluation, open_track_run, open_track_evaluation, byte_digest, strict_json)
109
+ from .packs import PackDescriptor, ExecutionPolicy
110
+ from pathlib import Path
111
+
112
+ PACK = PackDescriptor('rag.chunk.v1', 'v1', 'Source-bound chunking evaluation', 'rag_chunk',
113
+ 'sphynx.chunk.v1', 'sable.rag.dataset.v3', 'sphynx.chunk.request.v1', 'sphynx.chunk.response.v1',
114
+ 'runnable', 'Span and token recovery on frozen source documents.',
115
+ ExecutionPolicy('sphynx.chunk.policy.v1', 1, 30.0, 'trusted-local'),
116
+ metric_release='sphynx.chunk.metrics.v1', required_inputs=('Pinned DatasetV3',), run_api='run_chunking')
117
+
118
+ _source_evaluate_chunking = evaluate_chunking
119
+
120
+ def evaluate_chunking(documents, source_spans, evidence_groups, chunks, *, expected_document_sha256):
121
+ """Source-parity metrics with strict span, revision and chunk identity validation."""
122
+ if set(documents) != set(expected_document_sha256):
123
+ raise Level1Error('document hash inventory differs')
124
+ ids = [chunk.chunk_id for chunk in chunks]
125
+ if len(ids) != len(set(ids)) or any(not isinstance(i, str) or not i for i in ids):
126
+ raise Level1Error('chunk IDs must be unique nonempty strings')
127
+ for chunk in chunks:
128
+ if type(chunk.start) is not int or type(chunk.end) is not int:
129
+ raise Level1Error('chunk bounds must be integers')
130
+ span_ids = set()
131
+ for span in source_spans:
132
+ sid, did = span['source_span_id'], span['document_id']
133
+ if sid in span_ids or did not in documents:
134
+ raise Level1Error('duplicate span or unknown document')
135
+ span_ids.add(sid)
136
+ start, end = span['start'], span['end']
137
+ if type(start) is not int or type(end) is not int or not 0 <= start < end <= len(documents[did]):
138
+ raise Level1Error('invalid source span bounds')
139
+ if span.get('content', documents[did]) != documents[did] or span.get('exact_text', documents[did][start:end]) != documents[did][start:end]:
140
+ raise Level1Error('source span content/revision drift')
141
+ group_ids = set()
142
+ for group in evidence_groups:
143
+ gid = group['evidence_group_id']
144
+ if gid in group_ids or not set(group['source_span_ids']) <= span_ids:
145
+ raise Level1Error('duplicate group or unknown source reference')
146
+ group_ids.add(gid)
147
+ if group['requirement'] not in ('required','optional') or type(group['weight']) not in (int,float) or not math.isfinite(group['weight']) or group['weight'] <= 0:
148
+ raise Level1Error('invalid evidence group')
149
+ return _source_evaluate_chunking(documents, source_spans, evidence_groups, chunks,
150
+ expected_document_sha256=expected_document_sha256)
151
+
152
+
153
+ def _documents(task):
154
+ docs = {}
155
+ for span in task['source_spans']:
156
+ doc = {'document_id': span['document_id'], 'text': span['content'],
157
+ 'content_sha256': span['content_sha256'],
158
+ 'document_revision_family_id': span['document_revision_family_id']}
159
+ if doc['document_id'] in docs and docs[doc['document_id']] != doc:
160
+ raise ContractError('document revision drift')
161
+ docs[doc['document_id']] = doc
162
+ return list(docs.values())
163
+
164
+
165
+ def _request(task, policy):
166
+ return {'schema_version': 'sphynx.chunk.request.v1', 'case_id': task['task_id'],
167
+ 'documents': _documents(task), 'policy': policy}
168
+
169
+
170
+ def _parse_response(response, task):
171
+ if not isinstance(response, dict) or set(response) != {'schema_version','case_id','chunks'}:
172
+ raise ContractError('chunk response fields differ')
173
+ if response['schema_version'] != 'sphynx.chunk.response.v1' or response['case_id'] != task['task_id']:
174
+ raise ContractError('chunk response identity differs')
175
+ if not isinstance(response['chunks'], list):
176
+ raise ContractError('chunks must be an array')
177
+ chunks = []
178
+ for row in response['chunks']:
179
+ if not isinstance(row, dict) or set(row) != {'chunk_id','document_id','start','end','text'}:
180
+ raise ContractError('chunk fields differ')
181
+ chunks.append(Chunk(**row))
182
+ documents = {d['document_id']: d['text'] for d in _documents(task)}
183
+ hashes = {d['document_id']: d['content_sha256'] for d in _documents(task)}
184
+ return evaluate_chunking(documents, task['source_spans'], task['evidence_groups'], chunks,
185
+ expected_document_sha256=hashes)
186
+
187
+
188
+ def _policy(dataset, *, synthetic, owner_authorized, pack_manifest_path, trials, attempts, boundary_digest):
189
+ for count in (trials, attempts):
190
+ if type(count) is not int or count < 1:
191
+ raise ContractError('trials and attempts must be positive integers')
192
+ if type(synthetic) is not bool:
193
+ raise ContractError('synthetic must be explicit boolean')
194
+ approval = None
195
+ if not synthetic:
196
+ if owner_authorized is not True or pack_manifest_path is None:
197
+ raise ContractError('owner authorization and an approved pack manifest are required')
198
+ data = Path(pack_manifest_path).read_bytes()
199
+ pack = intake_validate_pack_manifest(strict_json(data))
200
+ if pack['dataset'] != {'dataset_id': dataset.dataset_id, 'version': dataset.version,
201
+ 'sha256': dataset._source_digest.removeprefix('sha256:')}:
202
+ raise ContractError('approved pack does not bind dataset bytes')
203
+ if any(t.customer_id != pack['customer_id'] for t in dataset.tasks):
204
+ raise ContractError('pack customer identity differs')
205
+ if scan_privacy(dataset.value):
206
+ raise ContractError('pack privacy scan failed')
207
+ if not assess_readiness(dataset, pack).quality_metrics_allowed:
208
+ raise ContractError('approved dataset remains blocked for quality claims')
209
+ approval = byte_digest(data)
210
+ return {'version':'sphynx.chunk.policy.v1', 'synthetic':synthetic,
211
+ 'approved_manifest_digest':approval, 'trials':trials, 'attempts':attempts,
212
+ 'selection':'first_success_per_trial', 'boundary_digest':boundary_digest}
213
+
214
+
215
+ def _identity(dataset, source_digest, tasks, policy):
216
+ return ComparisonIdentity('rag_chunk', PACK.pack_id, PACK.pack_release, 'sable.rag.dataset.v3',
217
+ source_digest, value_digest([t['task_id'] for t in tasks]),
218
+ value_digest([{'source_spans': t['source_spans'], 'evidence_groups': t['evidence_groups']} for t in tasks]),
219
+ value_digest(policy), PACK.metric_release, 'sphynx.chunk.grader.v1',
220
+ policy['boundary_digest'] or value_digest({'boundary':'chunker', 'dataset':source_digest}))
221
+
222
+
223
+ def run_chunking(candidate, adapter, dataset_path, output_dir, *, expected_sha256, synthetic=False,
224
+ owner_authorized=False, pack_manifest_path=None, trials=1, attempts=1,
225
+ case_ids=None, boundary_digest=None):
226
+ dataset = load_dataset_v3(dataset_path, expected_sha256=expected_sha256)
227
+ # A separate byte commitment is supplied to approval checks; no data identity is rewritten.
228
+ policy = _policy_for_source(dataset, expected_sha256, synthetic=synthetic,
229
+ owner_authorized=owner_authorized, pack_manifest_path=pack_manifest_path,
230
+ trials=trials, attempts=attempts, boundary_digest=boundary_digest)
231
+ tasks = _select_tasks(dataset, case_ids)
232
+ if not isinstance(candidate, CandidateConfiguration) or candidate.interface_version != PACK.interface_version:
233
+ raise ContractError('candidate must declare the chunk interface')
234
+ if candidate.execution_mode not in ('trusted_local', 'recorded'):
235
+ raise ContractError('chunk runner requires an explicit trusted local or recorded adapter')
236
+ if candidate.execution_mode == 'recorded':
237
+ if not isinstance(adapter, Mapping) or candidate.parameters.get('recordings_digest') != value_digest(adapter):
238
+ raise ContractError('recorded outputs need their complete pinned recording digest')
239
+ adapter = strict_json(__import__('json').dumps(dict(adapter),allow_nan=False))
240
+ elif not callable(adapter):
241
+ raise ContractError('trusted local adapter must be callable')
242
+ approval_source = None if synthetic else Path(pack_manifest_path).read_bytes().decode('utf-8')
243
+ observed = {'approved_manifest_source':approval_source}
244
+ _verify_approval(dataset, expected_sha256, policy, observed)
245
+ identity = _identity(dataset, expected_sha256, tasks, policy)
246
+ ordered = [t['task_id'] for t in tasks]
247
+ path = create_run(output_dir, identity, candidate, ordered)
248
+ captures=[]
249
+ for index, task in enumerate(tasks):
250
+ for trial in range(1,trials+1):
251
+ for attempt in range(1,attempts+1):
252
+ request = _request(task, policy)
253
+ prefix = f'cases/{index}/{trial}/{attempt}/'
254
+ request_binding = write_artifact(path,prefix+'request.json',request)
255
+ response_binding = None
256
+ failure=None
257
+ begin=perf_counter()
258
+ try:
259
+ response = adapter[value_digest(request)] if candidate.execution_mode == 'recorded' else adapter(strict_json(__import__('json').dumps(request)))
260
+ response_binding = write_artifact(path,prefix+'response.json',response)
261
+ _parse_response(response,task)
262
+ except Exception:
263
+ failure='invalid_response' if response_binding is not None else 'candidate_failure'
264
+ observed_binding=write_artifact(path,prefix+'observed.json',observed)
265
+ captures.append(TrackCaseCapture(task['task_id'],trial,attempt,request_binding,response_binding,observed_binding,failure,
266
+ (perf_counter()-begin)*1000))
267
+ return finish_run(path,identity,candidate,ordered,captures)
268
+
269
+
270
+ def _policy_for_source(dataset, source_digest, **kwargs):
271
+ # Pass immutable dataset and raw-file commitment together without altering DatasetV3.
272
+ class BoundDataset:
273
+ _source_digest=source_digest
274
+ dataset_id=dataset.dataset_id
275
+ version=dataset.version
276
+ tasks=dataset.tasks
277
+ value=dataset.value
278
+ return _policy(BoundDataset(), **kwargs)
279
+
280
+
281
+ def _select_tasks(dataset, case_ids):
282
+ tasks=dataset.value['tasks']
283
+ if case_ids is None: return tasks
284
+ case_ids=tuple(case_ids)
285
+ known={t['task_id']:t for t in tasks}
286
+ if not case_ids or len(set(case_ids))!=len(case_ids) or not set(case_ids)<=set(known):
287
+ raise ContractError('invalid selected case identities')
288
+ return [known[c] for c in case_ids]
289
+
290
+
291
+ def grade_chunking(run, dataset_path, *, expected_sha256):
292
+ dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
293
+ manifest=verify_track_run(run)
294
+ tasks=_select_tasks(dataset,manifest.case_ids)
295
+ policy=read_artifact(run.path,manifest.captures[0].request).get('policy')
296
+ if not isinstance(policy,dict) or set(policy)!={'version','synthetic','approved_manifest_digest','trials','attempts','selection','boundary_digest'}:
297
+ raise ContractError('capture policy fields differ')
298
+ if policy['version']!='sphynx.chunk.policy.v1' or policy['selection']!='first_success_per_trial' or type(policy['synthetic']) is not bool:
299
+ raise ContractError('unsupported capture policy')
300
+ require_comparable(_identity(dataset,expected_sha256,tasks,policy),manifest.identity)
301
+ manifest=verify_track_run(run,expected_case_ids=[t['task_id'] for t in tasks],
302
+ expected_requests={t['task_id']:_request(t,policy) for t in tasks},trials=policy['trials'],attempts=policy['attempts'])
303
+ for capture in manifest.captures:
304
+ if capture.observed is None:
305
+ raise ContractError('capture lacks its approval observation')
306
+ _verify_approval(dataset,expected_sha256,policy,read_artifact(run.path,capture.observed))
307
+ results=[]
308
+ for task in tasks:
309
+ for trial in range(1,policy['trials']+1):
310
+ selected=None
311
+ for capture in sorted(manifest.captures,key=lambda c:c.attempt_index):
312
+ if capture.case_id!=task['task_id'] or capture.trial_index!=trial: continue
313
+ if capture.failure is None and capture.response is not None:
314
+ score=_parse_response(read_artifact(run.path,capture.response),task)
315
+ if selected is None: selected=score
316
+ results.append({'case_id':task['task_id'],'trial_index':trial,'metrics':selected})
317
+ good=[r['metrics'] for r in results if r['metrics'] is not None]
318
+ summary={key: (sum(m[key] for m in good if m[key] is not None)/sum(m[key] is not None for m in good)
319
+ if any(m[key] is not None for m in good) else None)
320
+ for key in ('evidence_span_containment','gold_token_precision','gold_token_recall','evidence_density','duplicate_token_rate')}
321
+ summary.update(coverage=len(good)/len(results),successful_trials=len(good),expected_trials=len(results))
322
+ return write_evaluation(run,manifest.identity,{'summary':summary,'cases':results,
323
+ 'quality_scope':'synthetic_fixture' if policy['synthetic'] else 'owner_approved',
324
+ 'complete':len(good)==len(results)})
325
+
326
+
327
+ def report_chunking(evaluation, dataset_path, *, expected_sha256):
328
+ # Revalidate the complete capture/request boundary without invoking any candidate.
329
+ _verify_evaluation_dataset(evaluation,dataset_path,expected_sha256)
330
+ return verify_track_evaluation(evaluation).to_dict()
331
+
332
+
333
+ def _verify_evaluation_dataset(evaluation, dataset_path, expected_sha256):
334
+ dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256)
335
+ manifest=verify_track_run(evaluation.run)
336
+ policy=read_artifact(evaluation.run.path,manifest.captures[0].request)['policy']
337
+ tasks=_select_tasks(dataset,manifest.case_ids)
338
+ require_comparable(_identity(dataset,expected_sha256,tasks,policy),manifest.identity)
339
+ for capture in manifest.captures:
340
+ if capture.observed is None: raise ContractError('missing approval observation')
341
+ _verify_approval(dataset,expected_sha256,policy,read_artifact(evaluation.run.path,capture.observed))
342
+ verify_track_run(evaluation.run,expected_case_ids=[t['task_id'] for t in tasks],
343
+ expected_requests={t['task_id']:_request(t,policy) for t in tasks},trials=policy['trials'],attempts=policy['attempts'])
344
+
345
+
346
+ def compare_chunking(baseline,candidate,dataset_path,*,expected_sha256):
347
+ left=report_chunking(baseline,dataset_path,expected_sha256=expected_sha256)
348
+ right=report_chunking(candidate,dataset_path,expected_sha256=expected_sha256)
349
+ require_comparable(ComparisonIdentity.from_dict(left['identity']),ComparisonIdentity.from_dict(right['identity']))
350
+ return {'baseline':left['evaluation_id'],'candidate':right['evaluation_id'],
351
+ 'delta':{key: None if value is None or right['metrics']['summary'][key] is None
352
+ else right['metrics']['summary'][key]-value
353
+ for key,value in left['metrics']['summary'].items()}}
354
+
355
+
356
+ def _verify_approval(dataset, source_digest, policy, observed):
357
+ if not isinstance(observed,dict) or set(observed)!={'approved_manifest_source'}:
358
+ raise ContractError('approval observation fields differ')
359
+ source=observed['approved_manifest_source']
360
+ if policy['synthetic']:
361
+ if source is not None or policy['approved_manifest_digest'] is not None:
362
+ raise ContractError('synthetic captures cannot claim owner approval')
363
+ return
364
+ if not isinstance(source,str) or byte_digest(source.encode('utf-8'))!=policy['approved_manifest_digest']:
365
+ raise ContractError('approval evidence digest mismatch')
366
+ pack=intake_validate_pack_manifest(strict_json(source))
367
+ if pack['dataset']!={'dataset_id':dataset.dataset_id,'version':dataset.version,'sha256':source_digest.removeprefix('sha256:')}:
368
+ raise ContractError('approval evidence dataset mismatch')
369
+ if any(t.customer_id!=pack['customer_id'] for t in dataset.tasks):
370
+ raise ContractError('approval evidence customer mismatch')
371
+ if not assess_readiness(dataset,pack).quality_metrics_allowed:
372
+ raise ContractError('retained approval evidence does not permit quality claims')