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/components.py ADDED
@@ -0,0 +1,1661 @@
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
+ def canonical_json_bytes(value):
7
+ return json.dumps(value, allow_nan=False, ensure_ascii=False, separators=(",", ":"), sort_keys=True).encode("utf-8")
8
+ def json_sha256(value):
9
+ return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
10
+ """Versioned RAG component catalog and controlled batch-plan contracts.
11
+
12
+ The contract is intentionally declarative. It identifies implementations by registry
13
+ ID (or bounded command argv) and never dynamically imports or executes manifest data.
14
+ """
15
+ import hashlib
16
+ import json
17
+ import math
18
+ import re
19
+ from collections.abc import Mapping
20
+ from dataclasses import dataclass
21
+ from pathlib import Path, PurePosixPath, PureWindowsPath
22
+ from typing import Any, Literal, cast
23
+ COMPONENT_MANIFEST_SCHEMA_VERSION = 'sable.rag.component-manifest.v1'
24
+ EXPERIMENT_MANIFEST_SCHEMA_VERSION = 'sable.rag.experiment-manifest.v1'
25
+ DEFAULT_MODEL_POLICY_ID = 'minilm-jina-training-free-v1'
26
+ ComponentKind = Literal['chunker', 'dedup', 'embedder', 'full_rag_subject', 'fusion', 'gate', 'generator', 'reranker', 'retriever', 'rewriter', 'router']
27
+ ImplementationKind = Literal['builtin', 'command', 'recorded']
28
+ EvaluationLevel = Literal['level1', 'level2', 'level3']
29
+ EvaluationTrack = Literal['chunking', 'full_rag', 'pipeline', 'reranking', 'retrieval']
30
+ ComparisonMode = Literal['multi_component_diagnostic', 'single_component']
31
+ COMPONENT_KINDS: tuple[ComponentKind, ...] = ('chunker', 'embedder', 'retriever', 'reranker', 'fusion', 'dedup', 'gate', 'router', 'rewriter', 'generator', 'full_rag_subject')
32
+ IMPLEMENTATION_KINDS = frozenset({'builtin', 'command', 'recorded'})
33
+ TARGETS: dict[str, frozenset[str]] = {'level1': frozenset({'chunking', 'retrieval', 'reranking'}), 'level2': frozenset({'pipeline'}), 'level3': frozenset({'full_rag'})}
34
+ manifest_COMPONENT_KIND_SET = frozenset(COMPONENT_KINDS)
35
+ manifest_TARGET_CAPABLE_KINDS: dict[tuple[str, str], frozenset[str]] = {('level1', 'chunking'): frozenset({'chunker'}), ('level1', 'retrieval'): frozenset({'embedder', 'retriever'}), ('level1', 'reranking'): frozenset({'fusion', 'reranker'}), ('level2', 'pipeline'): frozenset({'chunker', 'dedup', 'embedder', 'fusion', 'gate', 'reranker', 'retriever'}), ('level3', 'full_rag'): manifest_COMPONENT_KIND_SET}
36
+ manifest_REQUIRED_KINDS: dict[tuple[str, str], frozenset[str]] = {('level1', 'chunking'): frozenset({'chunker'}), ('level1', 'retrieval'): frozenset({'embedder', 'retriever'}), ('level1', 'reranking'): frozenset({'reranker'}), ('level2', 'pipeline'): frozenset({'chunker', 'dedup', 'embedder', 'fusion', 'gate', 'reranker', 'retriever'}), ('level3', 'full_rag'): frozenset({'full_rag_subject'})}
37
+ manifest_MODEL_DECLARATION_REQUIRED_KINDS = frozenset({'embedder', 'generator', 'reranker'})
38
+ manifest_DEFAULT_RERANKER_FAMILIES = frozenset({'jina', 'minilm'})
39
+ manifest_SAFE_ID = re.compile('[a-z0-9][a-z0-9_-]{0,63}')
40
+ manifest_VERSION = re.compile('[A-Za-z0-9][A-Za-z0-9._+-]{0,127}')
41
+ manifest_SHA256 = re.compile('[0-9a-f]{64}')
42
+
43
+ class ComponentManifestError(ValueError):
44
+ """Raised when a component or batch manifest violates a control invariant."""
45
+
46
+ def manifest_mapping(value: object, context: str) -> Mapping[str, object]:
47
+ if not isinstance(value, Mapping) or any((not isinstance(key, str) for key in value)):
48
+ raise ComponentManifestError(f'{context} must be an object with string keys')
49
+ return cast('Mapping[str, object]', value)
50
+
51
+ def manifest_exact_keys(value: Mapping[str, object], required: frozenset[str], context: str) -> None:
52
+ missing = sorted(required - value.keys())
53
+ extra = sorted(value.keys() - required)
54
+ if missing or extra:
55
+ details: list[str] = []
56
+ if missing:
57
+ details.append(f"missing keys: {', '.join(missing)}")
58
+ if extra:
59
+ details.append(f"extra keys: {', '.join(extra)}")
60
+ raise ComponentManifestError(f"{context} has {'; '.join(details)}")
61
+
62
+ def manifest_text(value: object, context: str) -> str:
63
+ if not isinstance(value, str) or not value.strip():
64
+ raise ComponentManifestError(f'{context} must be a non-empty string')
65
+ return value
66
+
67
+ def manifest_safe_id(value: object, context: str) -> str:
68
+ text = manifest_text(value, context)
69
+ if manifest_SAFE_ID.fullmatch(text) is None:
70
+ raise ComponentManifestError(f'{context} must be a privacy-safe lowercase ID')
71
+ return text
72
+
73
+ def manifest_version(value: object, context: str) -> str:
74
+ text = manifest_text(value, context)
75
+ if manifest_VERSION.fullmatch(text) is None:
76
+ raise ComponentManifestError(f'{context} must be a pinned version identifier')
77
+ return text
78
+
79
+ def _manifest_sha256(value: object, context: str) -> str:
80
+ text = manifest_text(value, context)
81
+ if manifest_SHA256.fullmatch(text) is None:
82
+ raise ComponentManifestError(f'{context} must be a lowercase SHA-256 digest')
83
+ return text
84
+
85
+ def manifest_boolean(value: object, context: str) -> bool:
86
+ if not isinstance(value, bool):
87
+ raise ComponentManifestError(f'{context} must be a boolean')
88
+ return value
89
+
90
+ def manifest_json_value(value: object, context: str) -> JsonValue:
91
+ if value is None or isinstance(value, str | bool):
92
+ return value
93
+ if isinstance(value, int):
94
+ return value
95
+ if isinstance(value, float):
96
+ if not math.isfinite(value):
97
+ raise ComponentManifestError(f'{context} contains a non-finite number')
98
+ return value
99
+ if isinstance(value, list):
100
+ return [manifest_json_value(item, f'{context}[{index}]') for index, item in enumerate(value)]
101
+ if isinstance(value, Mapping) and all((isinstance(key, str) for key in value)):
102
+ string_mapping = cast('Mapping[str, object]', value)
103
+ return {key: manifest_json_value(item, f'{context}.{key}') for key, item in sorted(string_mapping.items())}
104
+ raise ComponentManifestError(f'{context} must contain only JSON values')
105
+
106
+ def manifest_string_list(value: object, context: str, *, nonempty: bool=True, unique: bool=True) -> tuple[str, ...]:
107
+ if not isinstance(value, list) or (nonempty and (not value)):
108
+ raise ComponentManifestError(f'{context} must be an array')
109
+ result = tuple((manifest_text(item, f'{context}[{index}]') for index, item in enumerate(value)))
110
+ if unique and len(result) != len(set(result)):
111
+ raise ComponentManifestError(f'{context} must contain unique values')
112
+ return result
113
+
114
+ def manifest_absolute_executable(value: str) -> bool:
115
+ return PurePosixPath(value).is_absolute() or PureWindowsPath(value).is_absolute()
116
+
117
+
118
+ def _record_mapping(value):
119
+ from dataclasses import fields
120
+ return {field.name: getattr(value, field.name) for field in fields(value)}
121
+
122
+
123
+ def _sequence(value, context, item_type, *, nonempty=False):
124
+ if not isinstance(value, (list, tuple)):
125
+ raise ComponentManifestError(f'{context} must be a list or tuple')
126
+ result = tuple(value)
127
+ if (nonempty and not result) or any(type(item) is not item_type for item in result):
128
+ raise ComponentManifestError(f'{context} contains invalid values')
129
+ return result
130
+
131
+
132
+ def _byte_object(value):
133
+ from ._track_capture import strict_json
134
+ if not isinstance(value, (bytes, bytearray, memoryview)):
135
+ raise ComponentManifestError('manifest content must be bytes')
136
+ encoded = bytes(value)
137
+ try:
138
+ raw = strict_json(encoded)
139
+ except ValueError as error:
140
+ raise ComponentManifestError('manifest content must be strict JSON') from error
141
+ manifest_mapping(raw, 'manifest content')
142
+ return encoded, raw
143
+
144
+
145
+ def _install_fields(value, names, normalized):
146
+ for name, item in zip(names, normalized, strict=True):
147
+ object.__setattr__(value, name, item)
148
+
149
+
150
+ @dataclass(frozen=True, slots=True, order=True)
151
+ class EvaluationTarget:
152
+ """One independently runnable causal level and track."""
153
+ level: EvaluationLevel
154
+ track: EvaluationTrack
155
+
156
+ def __post_init__(self) -> None:
157
+ raw = _record_mapping(self)
158
+ _install_fields(self, raw, _manifest_parse_target_fields(raw, 'EvaluationTarget'))
159
+
160
+ @property
161
+ def key(self) -> tuple[str, str]:
162
+ return (self.level, self.track)
163
+
164
+ @dataclass(frozen=True, slots=True)
165
+ class ImplementationSpec:
166
+ """Pinned implementation identity; no locator is interpreted by this module."""
167
+ kind: ImplementationKind
168
+ implementation_id: str
169
+ version: str
170
+ artifact_sha256: str
171
+ argv: tuple[str, ...]
172
+ def __post_init__(self) -> None:
173
+ raw = _record_mapping(self)
174
+ raw['argv'] = list(_sequence(self.argv, 'implementation.argv', str))
175
+ _install_fields(self, raw, _manifest_parse_implementation_fields(raw, 'ImplementationSpec'))
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class ModelSpec:
180
+ """Explicit model identity and whether an evaluation would require training."""
181
+ provider: str
182
+ family: str
183
+ model_id: str
184
+ revision: str
185
+ artifact_sha256: str
186
+ training_required: bool
187
+ def __post_init__(self) -> None:
188
+ raw = _record_mapping(self)
189
+ _install_fields(self, raw, _manifest_parse_model_fields(raw, 'ModelSpec'))
190
+
191
+
192
+ @dataclass(frozen=True, slots=True)
193
+ class ResourceContext:
194
+ """Pinned execution conditions required for comparable latency measurements."""
195
+ hardware: str
196
+ device: str
197
+ precision: str
198
+ batch_size: int
199
+ candidate_depth: int
200
+ warmup_runs: int
201
+ measurement_method: str
202
+ timeout_seconds: float
203
+ def __post_init__(self) -> None:
204
+ raw = _record_mapping(self)
205
+ _install_fields(self, raw, _manifest_parse_resource_context_fields(raw, 'ResourceContext'))
206
+
207
+
208
+ @dataclass(frozen=True, slots=True)
209
+ class ComponentSpec:
210
+ """One immutable, registry-addressed component configuration."""
211
+ component_id: str
212
+ version: str
213
+ kind: ComponentKind
214
+ implementation: ImplementationSpec
215
+ _config_bytes: bytes
216
+ config_sha256: str
217
+ resource_context: ResourceContext
218
+ capabilities: tuple[EvaluationTarget, ...]
219
+ model: ModelSpec | None
220
+
221
+ def __post_init__(self) -> None:
222
+ if type(self.implementation) is not ImplementationSpec or type(self.resource_context) is not ResourceContext or (self.model is not None and type(self.model) is not ModelSpec):
223
+ raise ComponentManifestError('component implementation, resource context or model is invalid')
224
+ _, config = _byte_object(self._config_bytes)
225
+ capabilities = _sequence(self.capabilities, 'component.capabilities', EvaluationTarget, nonempty=True)
226
+ raw = _record_mapping(self)
227
+ raw.pop('_config_bytes')
228
+ raw['config'] = config
229
+ raw['implementation'] = _record_mapping(self.implementation)
230
+ raw['implementation']['argv'] = list(self.implementation.argv)
231
+ raw['resource_context'] = _record_mapping(self.resource_context)
232
+ raw['model'] = None if self.model is None else _record_mapping(self.model)
233
+ raw['capabilities'] = [_record_mapping(item) for item in capabilities]
234
+ normalized = _manifest_parse_component_fields(raw, 'component')
235
+ _install_fields(self, _record_mapping(self), normalized)
236
+
237
+ @property
238
+ def config(self) -> dict[str, JsonValue]:
239
+ """Return a detached copy; callers cannot mutate the hash-bound configuration."""
240
+ return manifest_decode_object(self._config_bytes)
241
+
242
+ @dataclass(frozen=True, slots=True)
243
+ class ComponentManifest:
244
+ """Validated component registry whose hash pins a batch plan's full catalog."""
245
+ _value_bytes: bytes
246
+ manifest_id: str
247
+ version: str
248
+ components: tuple[ComponentSpec, ...]
249
+
250
+ def __post_init__(self) -> None:
251
+ encoded, raw = _byte_object(self._value_bytes)
252
+ components = _sequence(self.components, 'components', ComponentSpec, nonempty=True)
253
+ expected = _validate_component_manifest_fields(raw)
254
+ if (self.manifest_id, self.version, components) != expected[1:]:
255
+ raise ComponentManifestError('exposed component records differ from committed manifest bytes')
256
+ object.__setattr__(self, '_value_bytes', encoded)
257
+ object.__setattr__(self, 'components', components)
258
+
259
+ @property
260
+ def sha256(self) -> str:
261
+ return hashlib.sha256(self._value_bytes).hexdigest()
262
+
263
+ @property
264
+ def value(self) -> dict[str, JsonValue]:
265
+ """Return a detached manifest copy while immutable canonical bytes remain authoritative."""
266
+ return manifest_decode_object(self._value_bytes)
267
+
268
+ @property
269
+ def by_id(self) -> dict[str, ComponentSpec]:
270
+ return {component.component_id: component for component in self.components}
271
+
272
+ def to_mapping(self) -> dict[str, JsonValue]:
273
+ return manifest_decode_object(self._value_bytes)
274
+
275
+ @dataclass(frozen=True, slots=True)
276
+ class ComponentRef:
277
+ """Batch-plan pin to one catalog component."""
278
+ kind: ComponentKind
279
+ component_id: str
280
+ component_version: str
281
+ config_sha256: str
282
+
283
+ def __post_init__(self) -> None:
284
+ kind = manifest_text(self.kind, 'component reference kind')
285
+ if kind not in manifest_COMPONENT_KIND_SET:
286
+ raise ComponentManifestError('component reference kind is invalid')
287
+ manifest_safe_id(self.component_id, 'component reference ID')
288
+ manifest_version(self.component_version, 'component reference version')
289
+ _manifest_sha256(self.config_sha256, 'component reference config digest')
290
+
291
+ @property
292
+ def identity(self) -> tuple[str, str, str]:
293
+ return (self.component_id, self.component_version, self.config_sha256)
294
+
295
+ @dataclass(frozen=True, slots=True)
296
+ class ExperimentConfiguration:
297
+ """A complete assignment of component kinds to pinned catalog entries."""
298
+ configuration_id: str
299
+ components: tuple[ComponentRef, ...]
300
+
301
+ def __post_init__(self) -> None:
302
+ manifest_safe_id(self.configuration_id, 'configuration ID')
303
+ components = _sequence(self.components, 'configuration components', ComponentRef, nonempty=True)
304
+ if len({item.kind for item in components}) != len(components):
305
+ raise ComponentManifestError('configuration has duplicate component kinds')
306
+ object.__setattr__(self, 'components', components)
307
+
308
+ @property
309
+ def by_kind(self) -> dict[ComponentKind, ComponentRef]:
310
+ return {component.kind: component for component in self.components}
311
+
312
+ @dataclass(frozen=True, slots=True)
313
+ class ComponentChange:
314
+ """A change derived from exact baseline and alternative component identities."""
315
+ kind: ComponentKind
316
+ baseline: ComponentRef
317
+ alternative: ComponentRef
318
+ def __post_init__(self) -> None:
319
+ if type(self.baseline) is not ComponentRef or type(self.alternative) is not ComponentRef:
320
+ raise ComponentManifestError('component changes require component references')
321
+ if self.kind != self.baseline.kind or self.kind != self.alternative.kind or self.baseline.identity == self.alternative.identity:
322
+ raise ComponentManifestError('component change must bind different identities of the same kind')
323
+
324
+
325
+ @dataclass(frozen=True, slots=True)
326
+ class ExperimentAlternative:
327
+ """Named alternative with manifest-verified declared changes."""
328
+ alternative_id: str
329
+ comparison_mode: ComparisonMode
330
+ configuration: ExperimentConfiguration
331
+ changes: tuple[ComponentChange, ...]
332
+
333
+ def __post_init__(self) -> None:
334
+ manifest_safe_id(self.alternative_id, 'alternative ID')
335
+ mode = manifest_text(self.comparison_mode, 'comparison mode')
336
+ if mode not in {'single_component', 'multi_component_diagnostic'} or type(self.configuration) is not ExperimentConfiguration:
337
+ raise ComponentManifestError('invalid alternative mode or configuration')
338
+ changes = _sequence(self.changes, 'alternative changes', ComponentChange, nonempty=True)
339
+ if len({change.kind for change in changes}) != len(changes):
340
+ raise ComponentManifestError('alternative changes must have unique kinds')
341
+ if (mode == 'single_component' and len(changes) != 1) or (mode == 'multi_component_diagnostic' and len(changes) < 2):
342
+ raise ComponentManifestError('alternative change count differs from comparison mode')
343
+ if any(self.configuration.by_kind.get(change.kind) != change.alternative for change in changes):
344
+ raise ComponentManifestError('alternative changes differ from its configuration')
345
+ object.__setattr__(self, 'changes', changes)
346
+
347
+ @property
348
+ def decision_eligible(self) -> bool:
349
+ return self.comparison_mode == 'single_component'
350
+
351
+ @dataclass(frozen=True, slots=True)
352
+ class ExperimentManifest:
353
+ """Native batch plan over baseline/alternatives and runnable RAG targets.
354
+
355
+ BatchPlan is the canonical equivalent alias. Native experiment_id fields,
356
+ schema IDs, canonical bytes, and identity checks retain their meaning.
357
+ """
358
+ _value_bytes: bytes
359
+ experiment_id: str
360
+ version: str
361
+ component_manifest_sha256: str
362
+ targets: tuple[EvaluationTarget, ...]
363
+ baseline: ExperimentConfiguration
364
+ alternatives: tuple[ExperimentAlternative, ...]
365
+
366
+ def __post_init__(self) -> None:
367
+ encoded, raw = _byte_object(self._value_bytes)
368
+ targets = _sequence(self.targets, 'experiment targets', EvaluationTarget, nonempty=True)
369
+ alternatives = _sequence(self.alternatives, 'experiment alternatives', ExperimentAlternative, nonempty=True)
370
+ if type(self.baseline) is not ExperimentConfiguration:
371
+ raise ComponentManifestError('experiment baseline is invalid')
372
+ expected = _validate_experiment_manifest_fields(raw, None)
373
+ if (self.experiment_id, self.version, self.component_manifest_sha256, targets, self.baseline, alternatives) != expected[1:]:
374
+ raise ComponentManifestError('exposed experiment records differ from committed manifest bytes')
375
+ object.__setattr__(self, '_value_bytes', encoded)
376
+ object.__setattr__(self, 'targets', targets)
377
+ object.__setattr__(self, 'alternatives', alternatives)
378
+
379
+ @property
380
+ def sha256(self) -> str:
381
+ return hashlib.sha256(self._value_bytes).hexdigest()
382
+
383
+ @property
384
+ def value(self) -> dict[str, JsonValue]:
385
+ """Return a detached copy of the validated native batch declaration."""
386
+ return manifest_decode_object(self._value_bytes)
387
+
388
+ def changes_for(self, alternative_id: str) -> tuple[ComponentChange, ...]:
389
+ for alternative in self.alternatives:
390
+ if alternative.alternative_id == alternative_id:
391
+ return alternative.changes
392
+ raise KeyError(alternative_id)
393
+
394
+ def to_mapping(self) -> dict[str, JsonValue]:
395
+ return manifest_decode_object(self._value_bytes)
396
+
397
+ def manifest_decode_object(encoded: bytes) -> dict[str, JsonValue]:
398
+ return _byte_object(encoded)[1]
399
+
400
+
401
+ def _manifest_parse_target_fields(raw: object, context: str):
402
+ value = manifest_mapping(raw, context)
403
+ manifest_exact_keys(value, frozenset({'level', 'track'}), context)
404
+ level_text = manifest_text(value['level'], f'{context}.level')
405
+ track_text = manifest_text(value['track'], f'{context}.track')
406
+ if level_text not in TARGETS:
407
+ raise ComponentManifestError(f'{context}.level is invalid')
408
+ if track_text not in TARGETS[level_text]:
409
+ raise ComponentManifestError(f'{context}.track is invalid for {level_text}')
410
+ return (cast('EvaluationLevel', level_text), cast('EvaluationTrack', track_text))
411
+
412
+ def manifest_parse_target(raw: object, context: str) -> EvaluationTarget:
413
+ return EvaluationTarget(*_manifest_parse_target_fields(raw, context))
414
+
415
+ def _manifest_parse_implementation_fields(raw: object, context: str):
416
+ value = manifest_mapping(raw, context)
417
+ manifest_exact_keys(value, frozenset({'kind', 'implementation_id', 'version', 'artifact_sha256', 'argv'}), context)
418
+ kind_text = manifest_text(value['kind'], f'{context}.kind')
419
+ if kind_text not in IMPLEMENTATION_KINDS:
420
+ raise ComponentManifestError(f'{context}.kind is invalid')
421
+ argv = manifest_string_list(value['argv'], f'{context}.argv', nonempty=False, unique=False)
422
+ if len(argv) > 32 or any((len(argument) > 4096 or '\x00' in argument for argument in argv)):
423
+ raise ComponentManifestError(f'{context}.argv exceeds the bounded command contract')
424
+ if kind_text == 'command':
425
+ if not argv or not manifest_absolute_executable(argv[0]):
426
+ raise ComponentManifestError(f'{context}.argv needs an absolute executable for command implementations')
427
+ elif argv:
428
+ raise ComponentManifestError(f'{context}.argv must be empty unless implementation kind is command')
429
+ return (cast('ImplementationKind', kind_text), manifest_safe_id(value['implementation_id'], f'{context}.implementation_id'), manifest_version(value['version'], f'{context}.version'), _manifest_sha256(value['artifact_sha256'], f'{context}.artifact_sha256'), argv)
430
+
431
+ def manifest_parse_implementation(raw: object, context: str) -> ImplementationSpec:
432
+ return ImplementationSpec(*_manifest_parse_implementation_fields(raw, context))
433
+
434
+ def _manifest_parse_model_fields(raw: object, context: str):
435
+ if raw is None:
436
+ return None
437
+ value = manifest_mapping(raw, context)
438
+ manifest_exact_keys(value, frozenset({'provider', 'family', 'model_id', 'revision', 'artifact_sha256', 'training_required'}), context)
439
+ return (manifest_safe_id(value['provider'], f'{context}.provider'), manifest_safe_id(value['family'], f'{context}.family'), manifest_text(value['model_id'], f'{context}.model_id'), manifest_version(value['revision'], f'{context}.revision'), _manifest_sha256(value['artifact_sha256'], f'{context}.artifact_sha256'), manifest_boolean(value['training_required'], f'{context}.training_required'))
440
+
441
+ def manifest_parse_model(raw: object, context: str) -> ModelSpec | None:
442
+ fields = _manifest_parse_model_fields(raw, context)
443
+ return None if fields is None else ModelSpec(*fields)
444
+
445
+ def manifest_integer_at_least(value: object, minimum: int, context: str) -> int:
446
+ if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
447
+ raise ComponentManifestError(f'{context} must be an integer >= {minimum}')
448
+ return value
449
+
450
+ def _manifest_parse_resource_context_fields(raw: object, context: str):
451
+ value = manifest_mapping(raw, context)
452
+ manifest_exact_keys(value, frozenset({'hardware', 'device', 'precision', 'batch_size', 'candidate_depth', 'warmup_runs', 'measurement_method', 'timeout_seconds'}), context)
453
+ timeout = value['timeout_seconds']
454
+ if isinstance(timeout, bool) or not isinstance(timeout, int | float) or (not math.isfinite(float(timeout))) or (not 0 < float(timeout) <= 3600):
455
+ raise ComponentManifestError(f'{context}.timeout_seconds must be finite in (0, 3600]')
456
+ return (manifest_text(value['hardware'], f'{context}.hardware'), manifest_safe_id(value['device'], f'{context}.device'), manifest_safe_id(value['precision'], f'{context}.precision'), manifest_integer_at_least(value['batch_size'], 1, f'{context}.batch_size'), manifest_integer_at_least(value['candidate_depth'], 0, f'{context}.candidate_depth'), manifest_integer_at_least(value['warmup_runs'], 0, f'{context}.warmup_runs'), manifest_safe_id(value['measurement_method'], f'{context}.measurement_method'), float(timeout))
457
+
458
+ def manifest_parse_resource_context(raw: object, context: str) -> ResourceContext:
459
+ return ResourceContext(*_manifest_parse_resource_context_fields(raw, context))
460
+
461
+ def _manifest_parse_component_fields(raw: object, context: str):
462
+ value = manifest_mapping(raw, context)
463
+ manifest_exact_keys(value, frozenset({'component_id', 'version', 'kind', 'implementation', 'config', 'config_sha256', 'resource_context', 'capabilities', 'model'}), context)
464
+ kind_text = manifest_text(value['kind'], f'{context}.kind')
465
+ if kind_text not in manifest_COMPONENT_KIND_SET:
466
+ raise ComponentManifestError(f'{context}.kind is invalid')
467
+ kind = cast('ComponentKind', kind_text)
468
+ config = manifest_json_value(value['config'], f'{context}.config')
469
+ if not isinstance(config, dict):
470
+ raise ComponentManifestError(f'{context}.config must be an object')
471
+ config_sha256 = _manifest_sha256(value['config_sha256'], f'{context}.config_sha256')
472
+ if config_sha256 != json_sha256(config):
473
+ raise ComponentManifestError(f'{context}.config_sha256 does not bind config')
474
+ capabilities_raw = value['capabilities']
475
+ if not isinstance(capabilities_raw, list) or not capabilities_raw:
476
+ raise ComponentManifestError(f'{context}.capabilities must be a non-empty array')
477
+ capabilities = tuple((manifest_parse_target(item, f'{context}.capabilities[{index}]') for index, item in enumerate(capabilities_raw)))
478
+ if len(capabilities) != len(set(capabilities)):
479
+ raise ComponentManifestError(f'{context}.capabilities must be unique')
480
+ for capability in capabilities:
481
+ if kind not in manifest_TARGET_CAPABLE_KINDS[capability.key]:
482
+ raise ComponentManifestError(f'{context} kind {kind} has a capability/level mismatch for {capability.level}/{capability.track}')
483
+ model = manifest_parse_model(value['model'], f'{context}.model')
484
+ if kind in manifest_MODEL_DECLARATION_REQUIRED_KINDS and model is None:
485
+ raise ComponentManifestError(f'{context} kind {kind} must declare its model')
486
+ return (manifest_safe_id(value['component_id'], f'{context}.component_id'), manifest_version(value['version'], f'{context}.version'), kind, manifest_parse_implementation(value['implementation'], f'{context}.implementation'), canonical_json_bytes(config), config_sha256, manifest_parse_resource_context(value['resource_context'], f'{context}.resource_context'), capabilities, model)
487
+
488
+ def manifest_parse_component(raw: object, context: str) -> ComponentSpec:
489
+ return ComponentSpec(*_manifest_parse_component_fields(raw, context))
490
+
491
+ def _validate_component_manifest_fields(raw: object):
492
+ """Validate and normalize a versioned component catalog without loading code."""
493
+ root = manifest_mapping(raw, 'component manifest')
494
+ manifest_exact_keys(root, frozenset({'schema_version', 'manifest_id', 'version', 'components'}), 'component manifest')
495
+ if manifest_text(root['schema_version'], 'schema_version') != COMPONENT_MANIFEST_SCHEMA_VERSION:
496
+ raise ComponentManifestError(f'schema_version must be {COMPONENT_MANIFEST_SCHEMA_VERSION}')
497
+ components_raw = root['components']
498
+ if not isinstance(components_raw, list) or not components_raw:
499
+ raise ComponentManifestError('components must be a non-empty array')
500
+ components = tuple((manifest_parse_component(item, f'components[{index}]') for index, item in enumerate(components_raw)))
501
+ component_ids = tuple((component.component_id for component in components))
502
+ if len(component_ids) != len(set(component_ids)):
503
+ raise ComponentManifestError('component_id values must be unique')
504
+ normalized = manifest_json_value(root, 'component manifest')
505
+ if not isinstance(normalized, dict):
506
+ raise AssertionError('validated component manifest must remain an object')
507
+ return (canonical_json_bytes(normalized), manifest_safe_id(root['manifest_id'], 'manifest_id'), manifest_version(root['version'], 'version'), components)
508
+
509
+ def validate_component_manifest(raw: object) -> ComponentManifest:
510
+ """Validate and snapshot a catalog without loading component code."""
511
+ return ComponentManifest(*_validate_component_manifest_fields(raw))
512
+
513
+ def manifest_parse_component_ref(raw: object, context: str, catalog: Mapping[str, ComponentSpec] | None) -> ComponentRef:
514
+ value = manifest_mapping(raw, context)
515
+ manifest_exact_keys(value, frozenset({'kind', 'component_id', 'component_version', 'config_sha256'}), context)
516
+ kind_text = manifest_text(value['kind'], f'{context}.kind')
517
+ if kind_text not in manifest_COMPONENT_KIND_SET:
518
+ raise ComponentManifestError(f'{context}.kind is invalid')
519
+ kind = cast('ComponentKind', kind_text)
520
+ component_id = manifest_safe_id(value['component_id'], f'{context}.component_id')
521
+ if catalog is None:
522
+ return ComponentRef(kind, component_id, value['component_version'], value['config_sha256'])
523
+ component = catalog.get(component_id)
524
+ if component is None:
525
+ raise ComponentManifestError(f'{context} references unknown component {component_id}')
526
+ version = manifest_version(value['component_version'], f'{context}.component_version')
527
+ config_sha256 = _manifest_sha256(value['config_sha256'], f'{context}.config_sha256')
528
+ if kind != component.kind:
529
+ raise ComponentManifestError(f'{context}.kind does not match the catalog component')
530
+ if version != component.version:
531
+ raise ComponentManifestError(f'{context}.component_version does not match the catalog')
532
+ if config_sha256 != component.config_sha256:
533
+ raise ComponentManifestError(f'{context}.config_sha256 does not match the catalog')
534
+ return ComponentRef(kind, component_id, version, config_sha256)
535
+
536
+ def manifest_parse_configuration(raw: object, context: str, catalog: Mapping[str, ComponentSpec] | None) -> ExperimentConfiguration:
537
+ value = manifest_mapping(raw, context)
538
+ manifest_exact_keys(value, frozenset({'configuration_id', 'components'}), context)
539
+ components_raw = value['components']
540
+ if not isinstance(components_raw, list) or not components_raw:
541
+ raise ComponentManifestError(f'{context}.components must be a non-empty array')
542
+ components = tuple((manifest_parse_component_ref(item, f'{context}.components[{index}]', catalog) for index, item in enumerate(components_raw)))
543
+ kinds = tuple((component.kind for component in components))
544
+ if len(kinds) != len(set(kinds)):
545
+ raise ComponentManifestError(f'{context} has duplicate component kinds')
546
+ return ExperimentConfiguration(manifest_safe_id(value['configuration_id'], f'{context}.configuration_id'), components)
547
+
548
+ def manifest_validate_selection(raw: object) -> None:
549
+ selection = manifest_mapping(raw, 'configuration_selection')
550
+ manifest_exact_keys(selection, frozenset({'mode', 'scope', 'uses_query_or_task_features', 'uses_gold_or_evaluation_labels'}), 'configuration_selection')
551
+ if manifest_text(selection['mode'], 'configuration_selection.mode') != 'predeclared_global':
552
+ raise ComponentManifestError('configuration selection must be predeclared_global, never per-query')
553
+ if manifest_text(selection['scope'], 'configuration_selection.scope') != 'experiment':
554
+ raise ComponentManifestError('configuration selection scope must be experiment')
555
+ if manifest_boolean(selection['uses_query_or_task_features'], 'configuration_selection.uses_query_or_task_features'):
556
+ raise ComponentManifestError('per-query or per-task configuration selection is forbidden')
557
+ if manifest_boolean(selection['uses_gold_or_evaluation_labels'], 'configuration_selection.uses_gold_or_evaluation_labels'):
558
+ raise ComponentManifestError('gold-informed configuration selection is forbidden')
559
+
560
+ def manifest_validate_default_model_policy(raw: object) -> None:
561
+ policy = manifest_mapping(raw, 'model_policy')
562
+ manifest_exact_keys(policy, frozenset({'policy_id', 'allowed_reranker_families', 'training_allowed', 'require_reranker_model_declaration'}), 'model_policy')
563
+ if manifest_text(policy['policy_id'], 'model_policy.policy_id') != DEFAULT_MODEL_POLICY_ID:
564
+ raise ComponentManifestError(f'model_policy.policy_id must be {DEFAULT_MODEL_POLICY_ID}')
565
+ families = manifest_string_list(policy['allowed_reranker_families'], 'model_policy.allowed_reranker_families')
566
+ if frozenset(families) != manifest_DEFAULT_RERANKER_FAMILIES:
567
+ raise ComponentManifestError('default model policy allows only MiniLM and Jina rerankers')
568
+ if manifest_boolean(policy['training_allowed'], 'model_policy.training_allowed'):
569
+ raise ComponentManifestError('default model policy cannot allow training')
570
+ if not manifest_boolean(policy['require_reranker_model_declaration'], 'model_policy.require_reranker_model_declaration'):
571
+ raise ComponentManifestError('default model policy requires declared reranker models')
572
+
573
+ def manifest_validate_configuration_for_targets(configuration: ExperimentConfiguration, targets: tuple[EvaluationTarget, ...], catalog: Mapping[str, ComponentSpec] | None) -> None:
574
+ by_kind = configuration.by_kind
575
+ for target in targets:
576
+ missing = sorted(manifest_REQUIRED_KINDS[target.key] - by_kind.keys())
577
+ if missing:
578
+ raise ComponentManifestError(f"configuration {configuration.configuration_id} is missing required components for {target.level}/{target.track}: {', '.join(missing)}")
579
+ if catalog is None:
580
+ continue
581
+ for kind in manifest_REQUIRED_KINDS[target.key]:
582
+ component = catalog[by_kind[cast('ComponentKind', kind)].component_id]
583
+ if target not in component.capabilities:
584
+ raise ComponentManifestError(f'component {component.component_id} has a capability/level mismatch for {target.level}/{target.track}')
585
+
586
+ def manifest_validate_selected_models(configurations: tuple[ExperimentConfiguration, ...], catalog: Mapping[str, ComponentSpec]) -> None:
587
+ selected_ids = {reference.component_id for configuration in configurations for reference in configuration.components}
588
+ for component_id in sorted(selected_ids):
589
+ component = catalog[component_id]
590
+ if component.model is not None and component.model.training_required:
591
+ raise ComponentManifestError(f'component {component_id} requires training, which the default policy forbids')
592
+ if component.kind == 'reranker':
593
+ if component.model is None:
594
+ raise ComponentManifestError(f'reranker {component_id} must declare a model under the default policy')
595
+ if component.model.family not in manifest_DEFAULT_RERANKER_FAMILIES:
596
+ raise ComponentManifestError(f'reranker {component_id} uses undeclared model family {component.model.family}; only MiniLM and Jina are allowed')
597
+
598
+ def manifest_derive_changes(baseline: ExperimentConfiguration, alternative: ExperimentConfiguration) -> tuple[ComponentChange, ...]:
599
+ baseline_by_kind = baseline.by_kind
600
+ alternative_by_kind = alternative.by_kind
601
+ if baseline_by_kind.keys() != alternative_by_kind.keys():
602
+ raise ComponentManifestError(f'alternative {alternative.configuration_id} must assign exactly the baseline kinds')
603
+ changes: list[ComponentChange] = []
604
+ for kind in COMPONENT_KINDS:
605
+ before = baseline_by_kind.get(kind)
606
+ if before is None:
607
+ continue
608
+ after = alternative_by_kind[kind]
609
+ if before.identity != after.identity:
610
+ changes.append(ComponentChange(kind, before, after))
611
+ if not changes:
612
+ raise ComponentManifestError(f'alternative {alternative.configuration_id} is identical to the baseline')
613
+ return tuple(changes)
614
+
615
+ def _validate_experiment_manifest_fields(raw: object, component_manifest: ComponentManifest | None):
616
+ """Validate a fixed native batch plan and derive its component deltas."""
617
+ root = manifest_mapping(raw, 'experiment manifest')
618
+ manifest_exact_keys(root, frozenset({'schema_version', 'experiment_id', 'version', 'component_manifest', 'targets', 'configuration_selection', 'model_policy', 'baseline', 'alternatives'}), 'experiment manifest')
619
+ if manifest_text(root['schema_version'], 'schema_version') != EXPERIMENT_MANIFEST_SCHEMA_VERSION:
620
+ raise ComponentManifestError(f'schema_version must be {EXPERIMENT_MANIFEST_SCHEMA_VERSION}')
621
+ catalog_ref = manifest_mapping(root['component_manifest'], 'component_manifest')
622
+ manifest_exact_keys(catalog_ref, frozenset({'manifest_id', 'version', 'sha256'}), 'component_manifest')
623
+ catalog_id = manifest_safe_id(catalog_ref['manifest_id'], 'component_manifest.manifest_id')
624
+ if component_manifest is not None and catalog_id != component_manifest.manifest_id:
625
+ raise ComponentManifestError('component manifest ID does not match the catalog')
626
+ catalog_version = manifest_version(catalog_ref['version'], 'component_manifest.version')
627
+ if component_manifest is not None and catalog_version != component_manifest.version:
628
+ raise ComponentManifestError('component manifest version does not match the catalog')
629
+ manifest_sha256 = _manifest_sha256(catalog_ref['sha256'], 'component_manifest.sha256')
630
+ if component_manifest is not None and manifest_sha256 != component_manifest.sha256:
631
+ raise ComponentManifestError('component manifest SHA-256 does not match the catalog')
632
+ targets_raw = root['targets']
633
+ if not isinstance(targets_raw, list) or not targets_raw:
634
+ raise ComponentManifestError('targets must be a non-empty array')
635
+ targets = tuple((manifest_parse_target(item, f'targets[{index}]') for index, item in enumerate(targets_raw)))
636
+ if len(targets) != len(set(targets)):
637
+ raise ComponentManifestError('targets must be unique')
638
+ manifest_validate_selection(root['configuration_selection'])
639
+ manifest_validate_default_model_policy(root['model_policy'])
640
+ catalog = None if component_manifest is None else component_manifest.by_id
641
+ baseline = manifest_parse_configuration(root['baseline'], 'baseline', catalog)
642
+ manifest_validate_configuration_for_targets(baseline, targets, catalog)
643
+ alternatives_raw = root['alternatives']
644
+ if not isinstance(alternatives_raw, list) or not alternatives_raw:
645
+ raise ComponentManifestError('alternatives must be a non-empty array')
646
+ alternatives: list[ExperimentAlternative] = []
647
+ for index, raw_alternative in enumerate(alternatives_raw):
648
+ context = f'alternatives[{index}]'
649
+ value = manifest_mapping(raw_alternative, context)
650
+ manifest_exact_keys(value, frozenset({'alternative_id', 'comparison_mode', 'declared_changed_kinds', 'configuration'}), context)
651
+ alternative_id = manifest_safe_id(value['alternative_id'], f'{context}.alternative_id')
652
+ mode_text = manifest_text(value['comparison_mode'], f'{context}.comparison_mode')
653
+ if mode_text not in {'multi_component_diagnostic', 'single_component'}:
654
+ raise ComponentManifestError(f'{context}.comparison_mode is invalid')
655
+ mode = cast('ComparisonMode', mode_text)
656
+ configuration = manifest_parse_configuration(value['configuration'], f'{context}.configuration', catalog)
657
+ manifest_validate_configuration_for_targets(configuration, targets, catalog)
658
+ changes = manifest_derive_changes(baseline, configuration)
659
+ declared = manifest_string_list(value['declared_changed_kinds'], f'{context}.declared_changed_kinds')
660
+ derived = tuple((change.kind for change in changes))
661
+ if declared != derived:
662
+ raise ComponentManifestError(f"{context}.declared_changed_kinds does not match exact derived changes: {', '.join(derived)}")
663
+ if mode == 'single_component' and len(changes) != 1:
664
+ raise ComponentManifestError(f'{context} single_component comparison must change exactly one component')
665
+ if mode == 'multi_component_diagnostic' and len(changes) < 2:
666
+ raise ComponentManifestError(f'{context} multi_component_diagnostic must change at least two components')
667
+ if any((target.level == 'level2' for target in targets)) and mode != 'single_component':
668
+ raise ComponentManifestError(f'{context} Level 2 comparisons must change one component at a time')
669
+ for change in changes if catalog is not None else ():
670
+ before = catalog[change.baseline.component_id]
671
+ after = catalog[change.alternative.component_id]
672
+ if not any((target in before.capabilities and target in after.capabilities for target in targets)):
673
+ raise ComponentManifestError(f'changed component kind {change.kind} has no capability for a declared target')
674
+ alternatives.append(ExperimentAlternative(alternative_id, mode, configuration, changes))
675
+ identifiers = [baseline.configuration_id]
676
+ identifiers.extend((alternative.alternative_id for alternative in alternatives))
677
+ identifiers.extend((alternative.configuration.configuration_id for alternative in alternatives))
678
+ if len(identifiers) != len(set(identifiers)):
679
+ raise ComponentManifestError('baseline, alternative, and configuration IDs must be globally unique')
680
+ if catalog is not None:
681
+ manifest_validate_selected_models((baseline, *(alternative.configuration for alternative in alternatives)), catalog)
682
+ normalized = manifest_json_value(root, 'experiment manifest')
683
+ if not isinstance(normalized, dict):
684
+ raise AssertionError('validated experiment manifest must remain an object')
685
+ return (canonical_json_bytes(normalized), manifest_safe_id(root['experiment_id'], 'experiment_id'), manifest_version(root['version'], 'version'), manifest_sha256, targets, baseline, tuple(alternatives))
686
+
687
+ def validate_experiment_manifest(raw: object, component_manifest: ComponentManifest) -> ExperimentManifest:
688
+ """Validate the declaration and every referenced catalog identity and policy."""
689
+ if type(component_manifest) is not ComponentManifest:
690
+ raise ComponentManifestError('validated component manifest is required')
691
+ return ExperimentManifest(*_validate_experiment_manifest_fields(raw, component_manifest))
692
+
693
+ def manifest_reject_constant(value: str) -> None:
694
+ raise ComponentManifestError(f'non-finite JSON constant is forbidden: {value}')
695
+
696
+ def manifest_load_json(path: Path, context: str) -> object:
697
+ try:
698
+ return json.loads(path.read_text(encoding='utf-8'), parse_constant=manifest_reject_constant)
699
+ except (OSError, json.JSONDecodeError) as error:
700
+ raise ComponentManifestError(f'cannot read {context}: {error}') from error
701
+
702
+ def load_component_manifest(path: Path) -> ComponentManifest:
703
+ """Load and validate a local component manifest."""
704
+ return validate_component_manifest(manifest_load_json(path, 'component manifest'))
705
+
706
+ def load_experiment_manifest(path: Path, component_manifest: ComponentManifest) -> ExperimentManifest:
707
+ """Load a native batch plan against an already validated component catalog."""
708
+ return validate_experiment_manifest(manifest_load_json(path, 'experiment manifest'), component_manifest)
709
+
710
+ """Hermetic component adapter SDK for the offline RAG batch workbench.
711
+
712
+ Adapters are registered code objects, never import paths. Every registration is
713
+ checked against the already validated component manifest before an invocation can
714
+ run. The in-process and recorded implementations in this module perform no
715
+ network, subprocess, model-loading, or dynamic-import work.
716
+ """
717
+ import hashlib
718
+ import json
719
+ import math
720
+ import re
721
+ from collections.abc import Callable, Mapping, Sequence
722
+ from dataclasses import dataclass
723
+ from pathlib import Path
724
+ from typing import Any, Protocol, cast, runtime_checkable
725
+ AdapterHandler = Callable[[dict[str, JsonValue]], Mapping[str, object]]
726
+ sdk_SHA256 = re.compile('[0-9a-f]{64}')
727
+ sdk_TOKEN = re.compile('[a-z0-9]+')
728
+ OPERATIONS_BY_KIND: dict[ComponentKind, tuple[str, ...]] = {'chunker': ('chunk',), 'embedder': ('embed',), 'retriever': ('retrieve',), 'reranker': ('rerank',), 'fusion': ('fuse',), 'dedup': ('deduplicate',), 'gate': ('gate',), 'router': ('route',), 'rewriter': ('rewrite',), 'generator': ('generate',), 'full_rag_subject': ('full_rag',)}
729
+ PIPELINE_COMPONENT_ORDER: tuple[ComponentKind, ...] = ('chunker', 'embedder', 'retriever', 'reranker', 'fusion', 'dedup', 'gate')
730
+ SYNTHETIC_IMPLEMENTATION_IDS: dict[ComponentKind, str] = {kind: f"sable-synthetic-{kind.replace('_', '-')}-v1" for kind in COMPONENT_KINDS}
731
+
732
+ class ComponentSdkError(ValueError):
733
+ """Base error for invalid descriptors, registrations, and invocations."""
734
+
735
+ class AdapterRegistrationError(ComponentSdkError):
736
+ """Raised before execution when an adapter differs from its manifest entry."""
737
+
738
+ class AdapterInvocationError(ComponentSdkError):
739
+ """Raised when a bounded adapter request cannot produce a valid JSON result."""
740
+
741
+ def sdk_json_value(value: object, context: str) -> JsonValue:
742
+ if value is None or isinstance(value, str | bool):
743
+ return value
744
+ if isinstance(value, int):
745
+ return value
746
+ if isinstance(value, float):
747
+ if not math.isfinite(value):
748
+ raise AdapterInvocationError(f'{context} contains a non-finite number')
749
+ return value
750
+ if isinstance(value, list | tuple):
751
+ return [sdk_json_value(item, f'{context}[{index}]') for index, item in enumerate(value)]
752
+ if isinstance(value, Mapping) and all((isinstance(key, str) for key in value)):
753
+ string_mapping = cast('Mapping[str, object]', value)
754
+ return {key: sdk_json_value(item, f'{context}.{key}') for key, item in sorted(string_mapping.items())}
755
+ raise AdapterInvocationError(f'{context} must contain only finite JSON values')
756
+
757
+ def sdk_json_object(value: object, context: str) -> dict[str, JsonValue]:
758
+ normalized = sdk_json_value(value, context)
759
+ if not isinstance(normalized, dict):
760
+ raise AdapterInvocationError(f'{context} must be an object')
761
+ return normalized
762
+
763
+ def sdk_clone_object(value: Mapping[str, object], context: str) -> dict[str, JsonValue]:
764
+ normalized = sdk_json_object(value, context)
765
+ raw: Any = json.loads(canonical_json_bytes(normalized))
766
+ return cast('dict[str, JsonValue]', raw)
767
+
768
+ @dataclass(frozen=True, slots=True)
769
+ class AdapterDescriptor:
770
+ """Runtime identity bound one-to-one to a component manifest entry."""
771
+ component_id: str
772
+ component_version: str
773
+ component_kind: ComponentKind
774
+ implementation_id: str
775
+ implementation_kind: ImplementationKind
776
+ implementation_version: str
777
+ implementation_artifact_sha256: str
778
+ implementation_argv: tuple[str, ...]
779
+ config_sha256: str
780
+ resource_context: ResourceContext
781
+ capabilities: tuple[EvaluationTarget, ...]
782
+ operations: tuple[str, ...]
783
+
784
+ def __post_init__(self) -> None:
785
+
786
+ try:
787
+ ComponentRef(self.component_kind, self.component_id, self.component_version, self.config_sha256)
788
+ implementation = ImplementationSpec(self.implementation_kind, self.implementation_id, self.implementation_version, self.implementation_artifact_sha256, self.implementation_argv)
789
+ capabilities = _sequence(self.capabilities, 'adapter capabilities', EvaluationTarget, nonempty=True)
790
+ operations = _sequence(self.operations, 'adapter operations', str, nonempty=True)
791
+ if type(self.resource_context) is not ResourceContext:
792
+ raise ComponentManifestError('adapter resource context is invalid')
793
+ if any(self.component_kind not in manifest_TARGET_CAPABLE_KINDS[item.key] for item in capabilities):
794
+ raise ComponentManifestError('adapter capability does not match component kind')
795
+ object.__setattr__(self, 'implementation_argv', implementation.argv)
796
+ object.__setattr__(self, 'capabilities', capabilities)
797
+ object.__setattr__(self, 'operations', operations)
798
+ except ComponentManifestError as error:
799
+ raise AdapterRegistrationError(str(error)) from error
800
+ if not self.component_id or not self.component_version or (not self.implementation_id) or (not self.implementation_version):
801
+ raise AdapterRegistrationError('adapter descriptor identity must be non-empty')
802
+ if self.implementation_kind not in {'builtin', 'command', 'recorded'}:
803
+ raise AdapterRegistrationError('adapter descriptor implementation kind is invalid')
804
+ if (self.implementation_kind == 'command') != bool(self.implementation_argv):
805
+ raise AdapterRegistrationError('only command adapter descriptors may declare non-empty argv')
806
+ if sdk_SHA256.fullmatch(self.implementation_artifact_sha256) is None:
807
+ raise AdapterRegistrationError('adapter descriptor artifact hash must be SHA-256')
808
+ if sdk_SHA256.fullmatch(self.config_sha256) is None:
809
+ raise AdapterRegistrationError('adapter descriptor config hash must be SHA-256')
810
+ allowed = OPERATIONS_BY_KIND[self.component_kind]
811
+ if not self.operations or len(self.operations) != len(set(self.operations)):
812
+ raise AdapterRegistrationError('adapter descriptor operations must be non-empty and unique')
813
+ if any((operation not in allowed for operation in self.operations)):
814
+ raise AdapterRegistrationError(f'adapter descriptor operation is invalid for {self.component_kind}')
815
+ if not self.capabilities or len(self.capabilities) != len(set(self.capabilities)):
816
+ raise AdapterRegistrationError('adapter descriptor capabilities must be non-empty and unique')
817
+
818
+ @classmethod
819
+ def from_component(cls, component: ComponentSpec) -> AdapterDescriptor:
820
+ """Derive the only valid default runtime descriptor for a component."""
821
+ return cls(component.component_id, component.version, component.kind, component.implementation.implementation_id, component.implementation.kind, component.implementation.version, component.implementation.artifact_sha256, component.implementation.argv, component.config_sha256, component.resource_context, component.capabilities, OPERATIONS_BY_KIND[component.kind])
822
+
823
+ @runtime_checkable
824
+ class ComponentAdapter(Protocol):
825
+ """Universal synchronous component surface consumed by the workbench."""
826
+
827
+ @property
828
+ def descriptor(self) -> AdapterDescriptor:
829
+ ...
830
+
831
+ @property
832
+ def capabilities(self) -> tuple[EvaluationTarget, ...]:
833
+ ...
834
+
835
+ def invoke(self, operation: str, payload: Mapping[str, object]) -> dict[str, JsonValue]:
836
+ ...
837
+
838
+ def canonical_request_sha256(descriptor: AdapterDescriptor, operation: str, payload: Mapping[str, object]) -> str:
839
+ """Hash the complete adapter identity, operation, and canonical JSON payload."""
840
+ if operation not in descriptor.operations:
841
+ raise AdapterInvocationError(f'operation {operation!r} is not supported by {descriptor.component_id}')
842
+ normalized = sdk_json_object(payload, 'adapter request payload')
843
+ request: dict[str, JsonValue] = {'component_id': descriptor.component_id, 'component_version': descriptor.component_version, 'config_sha256': descriptor.config_sha256, 'implementation_argv': list(descriptor.implementation_argv), 'implementation_id': descriptor.implementation_id, 'implementation_version': descriptor.implementation_version, 'operation': operation, 'payload': normalized, 'resource_context': {'hardware': descriptor.resource_context.hardware, 'device': descriptor.resource_context.device, 'precision': descriptor.resource_context.precision, 'batch_size': descriptor.resource_context.batch_size, 'candidate_depth': descriptor.resource_context.candidate_depth, 'warmup_runs': descriptor.resource_context.warmup_runs, 'measurement_method': descriptor.resource_context.measurement_method, 'timeout_seconds': descriptor.resource_context.timeout_seconds}}
844
+ return hashlib.sha256(canonical_json_bytes(request)).hexdigest()
845
+
846
+ class FunctionAdapter:
847
+ """Registered in-process handlers with canonical I/O and replay determinism checks."""
848
+ __slots__ = ('_descriptor', '_handlers', '_observed')
849
+
850
+ def __init__(self, descriptor: AdapterDescriptor, handlers: Mapping[str, AdapterHandler]) -> None:
851
+ if descriptor.implementation_kind != 'builtin':
852
+ raise AdapterRegistrationError('FunctionAdapter requires a builtin implementation')
853
+ if set(handlers) != set(descriptor.operations):
854
+ raise AdapterRegistrationError('FunctionAdapter handlers must exactly match descriptor operations')
855
+ self._descriptor = descriptor
856
+ self._handlers = dict(handlers)
857
+ self._observed: dict[str, bytes] = {}
858
+
859
+ @property
860
+ def descriptor(self) -> AdapterDescriptor:
861
+ return self._descriptor
862
+
863
+ @property
864
+ def capabilities(self) -> tuple[EvaluationTarget, ...]:
865
+ return self._descriptor.capabilities
866
+
867
+ def invoke(self, operation: str, payload: Mapping[str, object]) -> dict[str, JsonValue]:
868
+ handler = self._handlers.get(operation)
869
+ if handler is None:
870
+ raise AdapterInvocationError(f'operation {operation!r} is not supported by {self._descriptor.component_id}')
871
+ normalized_payload = sdk_clone_object(payload, 'adapter request payload')
872
+ request_hash = canonical_request_sha256(self._descriptor, operation, normalized_payload)
873
+ try:
874
+ raw = handler(sdk_clone_object(normalized_payload, 'adapter request payload'))
875
+ except ComponentSdkError:
876
+ raise
877
+ except Exception as error:
878
+ raise AdapterInvocationError(f'builtin adapter {self._descriptor.component_id} failed') from error
879
+ output = sdk_json_object(raw, 'adapter response')
880
+ encoded = canonical_json_bytes(output)
881
+ previous = self._observed.get(request_hash)
882
+ if previous is not None and previous != encoded:
883
+ raise AdapterInvocationError(f'builtin adapter {self._descriptor.component_id} is nondeterministic')
884
+ self._observed[request_hash] = encoded
885
+ decoded: Any = json.loads(encoded)
886
+ return cast('dict[str, JsonValue]', decoded)
887
+
888
+ class RecordedAdapter:
889
+ """Read-only response table indexed by a content-bound canonical request hash."""
890
+ __slots__ = ('_descriptor', '_records')
891
+
892
+ def __init__(self, descriptor: AdapterDescriptor, records: Mapping[str, Mapping[str, object]]) -> None:
893
+ if descriptor.implementation_kind != 'recorded':
894
+ raise AdapterRegistrationError('RecordedAdapter requires a recorded implementation')
895
+ normalized: dict[str, bytes] = {}
896
+ artifact: dict[str, JsonValue] = {}
897
+ for request_hash, response in records.items():
898
+ if sdk_SHA256.fullmatch(request_hash) is None:
899
+ raise AdapterRegistrationError('recorded request keys must be SHA-256 digests')
900
+ value = sdk_json_object(response, f'recorded response {request_hash}')
901
+ normalized[request_hash] = canonical_json_bytes(value)
902
+ artifact[request_hash] = value
903
+ if not normalized:
904
+ raise AdapterRegistrationError('RecordedAdapter requires at least one response')
905
+ if json_sha256(artifact) != descriptor.implementation_artifact_sha256:
906
+ raise AdapterRegistrationError('recorded responses do not match the declared implementation artifact hash')
907
+ self._descriptor = descriptor
908
+ self._records = normalized
909
+
910
+ @property
911
+ def descriptor(self) -> AdapterDescriptor:
912
+ return self._descriptor
913
+
914
+ @property
915
+ def capabilities(self) -> tuple[EvaluationTarget, ...]:
916
+ return self._descriptor.capabilities
917
+
918
+ def invoke(self, operation: str, payload: Mapping[str, object]) -> dict[str, JsonValue]:
919
+ request_hash = canonical_request_sha256(self._descriptor, operation, payload)
920
+ encoded = self._records.get(request_hash)
921
+ if encoded is None:
922
+ raise AdapterInvocationError(f'recorded adapter {self._descriptor.component_id} has no matching request')
923
+ raw: Any = json.loads(encoded)
924
+ return cast('dict[str, JsonValue]', raw)
925
+
926
+ class ComponentRegistry:
927
+ """Manifest-bound adapter registry with deterministic lookup and listing."""
928
+
929
+ def __init__(self, manifest: ComponentManifest) -> None:
930
+ self._manifest = manifest
931
+ for component in manifest.components:
932
+ if json_sha256(component.config) != component.config_sha256:
933
+ raise AdapterRegistrationError(f'component {component.component_id} config no longer matches its hash')
934
+ self._expected = {component.component_id: AdapterDescriptor.from_component(component) for component in manifest.components}
935
+ self._adapters: dict[str, ComponentAdapter] = {}
936
+
937
+ @property
938
+ def manifest(self) -> ComponentManifest:
939
+ return self._manifest
940
+
941
+ def register(self, adapter: ComponentAdapter) -> None:
942
+ descriptor = adapter.descriptor
943
+ expected = self._expected.get(descriptor.component_id)
944
+ if expected is None:
945
+ raise AdapterRegistrationError(f'adapter {descriptor.component_id} is absent from the component manifest')
946
+ if descriptor != expected:
947
+ raise AdapterRegistrationError(f'adapter {descriptor.component_id} descriptor differs from its manifest entry')
948
+ if adapter.capabilities != expected.capabilities:
949
+ raise AdapterRegistrationError(f'adapter {descriptor.component_id} capabilities differ from its descriptor')
950
+ if descriptor.component_id in self._adapters:
951
+ raise AdapterRegistrationError(f'adapter {descriptor.component_id} is already registered')
952
+ self._adapters[descriptor.component_id] = adapter
953
+
954
+ def resolve(self, component_id: str, target: EvaluationTarget, operation: str) -> ComponentAdapter:
955
+ adapter = self._adapters.get(component_id)
956
+ if adapter is None:
957
+ if component_id not in self._expected:
958
+ raise AdapterRegistrationError(f'component {component_id} is absent from the component manifest')
959
+ raise AdapterRegistrationError(f'component {component_id} has no registered adapter')
960
+ expected = self._expected[component_id]
961
+ component = self._manifest.by_id[component_id]
962
+ if json_sha256(component.config) != component.config_sha256:
963
+ raise AdapterRegistrationError(f'component {component_id} config no longer matches its hash')
964
+ if adapter.descriptor != expected or adapter.capabilities != expected.capabilities:
965
+ raise AdapterRegistrationError(f'registered adapter {component_id} descriptor drifted')
966
+ if target not in expected.capabilities:
967
+ raise AdapterRegistrationError(f'component {component_id} does not support {target.level}/{target.track}')
968
+ if operation not in expected.operations:
969
+ raise AdapterRegistrationError(f'component {component_id} does not support operation {operation!r}')
970
+ return adapter
971
+
972
+ def list(self) -> tuple[AdapterDescriptor, ...]:
973
+ """Return registered descriptors in stable component-ID order."""
974
+ return tuple((self._adapters[component_id].descriptor for component_id in sorted(self._adapters)))
975
+
976
+ def sdk_text(value: object, context: str) -> str:
977
+ if not isinstance(value, str):
978
+ raise AdapterInvocationError(f'{context} must be text')
979
+ return value
980
+
981
+ def sdk_number(value: object, context: str) -> float:
982
+ if isinstance(value, bool) or not isinstance(value, int | float):
983
+ raise AdapterInvocationError(f'{context} must be numeric')
984
+ result = float(value)
985
+ if not math.isfinite(result):
986
+ raise AdapterInvocationError(f'{context} must be finite')
987
+ return result
988
+
989
+ def sdk_integer_config(component: ComponentSpec, key: str, default: int) -> int:
990
+ value = component.config.get(key, default)
991
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
992
+ raise AdapterRegistrationError(f'{component.component_id} config.{key} must be positive')
993
+ return value
994
+
995
+ def sdk_number_config(component: ComponentSpec, key: str, default: float) -> float:
996
+ value = component.config.get(key, default)
997
+ return sdk_number(value, f'{component.component_id} config.{key}')
998
+
999
+ def sdk_rows(payload: Mapping[str, object], key: str) -> list[dict[str, JsonValue]]:
1000
+ value = payload.get(key)
1001
+ if not isinstance(value, list):
1002
+ raise AdapterInvocationError(f'payload.{key} must be an array')
1003
+ rows: list[dict[str, JsonValue]] = []
1004
+ for index, row in enumerate(value):
1005
+ rows.append(sdk_json_object(row, f'payload.{key}[{index}]'))
1006
+ return rows
1007
+
1008
+ def sdk_candidate_id(row: Mapping[str, object], index: int) -> str:
1009
+ value = row.get('candidate_id', row.get('chunk_id', f'candidate-{index:04d}'))
1010
+ return sdk_text(value, f'candidate[{index}].candidate_id')
1011
+
1012
+ def sdk_candidate_text(row: Mapping[str, object], index: int) -> str:
1013
+ value = row.get('contextualized_text', row.get('input', row.get('text')))
1014
+ return sdk_text(value, f'candidate[{index}].text')
1015
+
1016
+ def sdk_tokens(text: str) -> frozenset[str]:
1017
+ return frozenset(sdk_TOKEN.findall(text.lower()))
1018
+
1019
+ def sdk_lexical_score(query: str, text: str) -> float:
1020
+ query_tokens = sdk_tokens(query)
1021
+ if not query_tokens:
1022
+ return 0.0
1023
+ return len(query_tokens & sdk_tokens(text)) / len(query_tokens)
1024
+
1025
+ def sdk_synthetic_chunker(component: ComponentSpec) -> AdapterHandler:
1026
+ chunk_size = sdk_integer_config(component, 'chunk_size', 256)
1027
+ raw_overlap = component.config.get('overlap', 0)
1028
+ if isinstance(raw_overlap, bool) or not isinstance(raw_overlap, int):
1029
+ raise AdapterRegistrationError(f'{component.component_id} config.overlap must be an integer')
1030
+ if raw_overlap < 0 or raw_overlap >= chunk_size:
1031
+ raise AdapterRegistrationError(f'{component.component_id} config.overlap must be in [0, chunk_size)')
1032
+ step = chunk_size - raw_overlap
1033
+
1034
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1035
+ documents = sdk_rows(cast('Mapping[str, object]', payload), 'documents')
1036
+ chunks: list[dict[str, JsonValue]] = []
1037
+ for document_index, document in enumerate(documents):
1038
+ document_id = sdk_text(document.get('document_id'), f'documents[{document_index}].document_id')
1039
+ text = sdk_text(document.get('text'), f'documents[{document_index}].text')
1040
+ for chunk_index, start in enumerate(range(0, len(text), step)):
1041
+ end = min(start + chunk_size, len(text))
1042
+ chunk_id = f'{document_id}:chunk:{chunk_index:04d}'
1043
+ chunk_text = text[start:end]
1044
+ chunks.append({'candidate_id': chunk_id, 'chunk_id': chunk_id, 'document_id': document_id, 'end': end, 'input': chunk_text, 'start': start, 'text': chunk_text})
1045
+ if end == len(text):
1046
+ break
1047
+ return {'chunks': chunks}
1048
+ return invoke
1049
+
1050
+ def sdk_synthetic_embedder(component: ComponentSpec) -> AdapterHandler:
1051
+ dimensions = sdk_integer_config(component, 'dimensions', 4)
1052
+ if dimensions > 32:
1053
+ raise AdapterRegistrationError(f'{component.component_id} config.dimensions must not exceed 32')
1054
+
1055
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1056
+ items = sdk_rows(cast('Mapping[str, object]', payload), 'items')
1057
+ embeddings: list[dict[str, JsonValue]] = []
1058
+ for index, item in enumerate(items):
1059
+ item_id = sdk_candidate_id(cast('Mapping[str, object]', item), index)
1060
+ text = sdk_candidate_text(cast('Mapping[str, object]', item), index)
1061
+ digest = hashlib.sha256(text.encode('utf-8')).digest()
1062
+ values = [digest[position] / 127.5 - 1.0 for position in range(dimensions)]
1063
+ norm = math.sqrt(sum((value * value for value in values))) or 1.0
1064
+ vector: list[JsonValue] = [round(value / norm, 12) for value in values]
1065
+ embeddings.append({'candidate_id': item_id, 'vector': vector})
1066
+ return {'embeddings': embeddings}
1067
+ return invoke
1068
+
1069
+ def sdk_synthetic_ranker(component: ComponentSpec, *, reranker: bool) -> AdapterHandler:
1070
+ reverse = component.config.get('reverse', False)
1071
+ if not isinstance(reverse, bool):
1072
+ raise AdapterRegistrationError(f'{component.component_id} config.reverse must be boolean')
1073
+ score_field = 'reranker_score' if reranker else 'dense_score'
1074
+ rank_field = 'reranker_rank' if reranker else 'dense_rank'
1075
+
1076
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1077
+ query = sdk_text(payload.get('query'), 'payload.query')
1078
+ candidates = sdk_rows(cast('Mapping[str, object]', payload), 'candidates')
1079
+ scored: list[tuple[float, str, dict[str, JsonValue]]] = []
1080
+ for index, candidate in enumerate(candidates):
1081
+ identity = sdk_candidate_id(cast('Mapping[str, object]', candidate), index)
1082
+ text = sdk_candidate_text(cast('Mapping[str, object]', candidate), index)
1083
+ score = sdk_lexical_score(query, text)
1084
+ if reverse:
1085
+ score = 1.0 - score
1086
+ scored.append((score, identity, dict(candidate)))
1087
+ scored.sort(key=lambda item: (-item[0], item[1]))
1088
+ result: list[dict[str, JsonValue]] = []
1089
+ for rank, (score, _, candidate) in enumerate(scored, start=1):
1090
+ candidate[score_field] = score
1091
+ candidate[rank_field] = rank
1092
+ result.append(candidate)
1093
+ return {'candidates': result}
1094
+ return invoke
1095
+
1096
+ def sdk_synthetic_fusion(component: ComponentSpec) -> AdapterHandler:
1097
+ dense_weight = sdk_number_config(component, 'dense_weight', 0.45)
1098
+ reranker_weight = sdk_number_config(component, 'reranker_weight', 0.55)
1099
+ if dense_weight < 0 or reranker_weight < 0 or dense_weight + reranker_weight <= 0:
1100
+ raise AdapterRegistrationError(f'{component.component_id} fusion weights are invalid')
1101
+
1102
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1103
+ candidates = sdk_rows(cast('Mapping[str, object]', payload), 'candidates')
1104
+ scored: list[tuple[float, str, dict[str, JsonValue]]] = []
1105
+ for index, candidate in enumerate(candidates):
1106
+ identity = sdk_candidate_id(cast('Mapping[str, object]', candidate), index)
1107
+ dense = sdk_number(candidate.get('dense_score'), f'candidate[{index}].dense_score')
1108
+ reranker_score = sdk_number(candidate.get('reranker_score'), f'candidate[{index}].reranker_score')
1109
+ score = dense_weight * dense + reranker_weight * reranker_score
1110
+ scored.append((score, identity, dict(candidate)))
1111
+ scored.sort(key=lambda item: (-item[0], item[1]))
1112
+ result: list[dict[str, JsonValue]] = []
1113
+ for rank, (score, _, candidate) in enumerate(scored, start=1):
1114
+ candidate['fusion_score'] = score
1115
+ candidate['fusion_rank'] = rank
1116
+ result.append(candidate)
1117
+ return {'candidates': result}
1118
+ return invoke
1119
+
1120
+ def sdk_synthetic_dedup(_: ComponentSpec) -> AdapterHandler:
1121
+
1122
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1123
+ candidates = sdk_rows(cast('Mapping[str, object]', payload), 'candidates')
1124
+ seen: set[str] = set()
1125
+ next_rank = 1
1126
+ result: list[dict[str, JsonValue]] = []
1127
+ for index, candidate in enumerate(candidates):
1128
+ passage = sdk_candidate_text(cast('Mapping[str, object]', candidate), index)
1129
+ row = dict(candidate)
1130
+ if passage in seen:
1131
+ row['dedup_decision'] = 'drop'
1132
+ row['post_dedup_rank'] = None
1133
+ else:
1134
+ seen.add(passage)
1135
+ row['dedup_decision'] = 'keep'
1136
+ row['post_dedup_rank'] = next_rank
1137
+ next_rank += 1
1138
+ result.append(row)
1139
+ return {'candidates': result}
1140
+ return invoke
1141
+
1142
+ def sdk_synthetic_gate(component: ComponentSpec) -> AdapterHandler:
1143
+ threshold = sdk_number_config(component, 'threshold', 0.0)
1144
+ top_k = sdk_integer_config(component, 'top_k', 5)
1145
+
1146
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1147
+ candidates = sdk_rows(cast('Mapping[str, object]', payload), 'candidates')
1148
+ next_rank = 1
1149
+ selected: list[str] = []
1150
+ result: list[dict[str, JsonValue]] = []
1151
+ for index, candidate in enumerate(candidates):
1152
+ row = dict(candidate)
1153
+ kept = row.get('dedup_decision', 'keep') == 'keep'
1154
+ raw_score = row.get('fusion_score', row.get('reranker_score', row.get('score', 0.0)))
1155
+ score = sdk_number(raw_score, f'candidate[{index}].gate_score')
1156
+ passed = kept and score >= threshold
1157
+ row['gate_decision'] = 'pass' if passed else 'reject'
1158
+ row['gate_threshold'] = threshold
1159
+ if passed and next_rank <= top_k:
1160
+ identity = sdk_candidate_id(cast('Mapping[str, object]', row), index)
1161
+ row['final_rank'] = next_rank
1162
+ selected.append(identity)
1163
+ next_rank += 1
1164
+ else:
1165
+ row['final_rank'] = None
1166
+ result.append(row)
1167
+ return {'candidates': result, 'selected_candidate_ids': selected}
1168
+ return invoke
1169
+
1170
+ def sdk_synthetic_router(component: ComponentSpec) -> AdapterHandler:
1171
+ should_call = component.config.get('should_call_rag', True)
1172
+ if not isinstance(should_call, bool):
1173
+ raise AdapterRegistrationError(f'{component.component_id} config.should_call_rag must be boolean')
1174
+
1175
+ def invoke(_: dict[str, JsonValue]) -> Mapping[str, object]:
1176
+ return {'should_call_rag': should_call}
1177
+ return invoke
1178
+
1179
+ def sdk_synthetic_rewriter(_: ComponentSpec) -> AdapterHandler:
1180
+
1181
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1182
+ query = sdk_text(payload.get('query'), 'payload.query')
1183
+ return {'query': query, 'rewritten_query': ' '.join(query.split())}
1184
+ return invoke
1185
+
1186
+ def sdk_synthetic_generator(component: ComponentSpec) -> AdapterHandler:
1187
+ prefix_value = component.config.get('answer_prefix', '')
1188
+ prefix = sdk_text(prefix_value, f'{component.component_id} config.answer_prefix')
1189
+
1190
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1191
+ contexts = sdk_rows(cast('Mapping[str, object]', payload), 'contexts')
1192
+ passages = [sdk_candidate_text(cast('Mapping[str, object]', context), index) for index, context in enumerate(contexts)]
1193
+ answer = ' '.join((part for part in (prefix, *passages) if part)).strip()
1194
+ return {'answer': answer, 'abstained': not passages}
1195
+ return invoke
1196
+
1197
+ def sdk_synthetic_full_rag(_: ComponentSpec) -> AdapterHandler:
1198
+
1199
+ def invoke(payload: dict[str, JsonValue]) -> Mapping[str, object]:
1200
+ query = sdk_text(payload.get('query'), 'payload.query')
1201
+ contexts = sdk_rows(cast('Mapping[str, object]', payload), 'contexts')
1202
+ context_ids = [sdk_candidate_id(cast('Mapping[str, object]', context), index) for index, context in enumerate(contexts)]
1203
+ answer = ' '.join((sdk_candidate_text(cast('Mapping[str, object]', context), index) for index, context in enumerate(contexts)))
1204
+ return {'actual_should_call_rag': bool(contexts), 'answer': answer, 'citations': context_ids, 'handoff_candidate_ids': context_ids, 'retrieved_candidate_ids': context_ids, 'rewritten_query': query, 'uttered_query': query}
1205
+ return invoke
1206
+
1207
+ def sdk_synthetic_handler(component: ComponentSpec) -> AdapterHandler:
1208
+ if component.kind == 'chunker':
1209
+ return sdk_synthetic_chunker(component)
1210
+ if component.kind == 'embedder':
1211
+ return sdk_synthetic_embedder(component)
1212
+ if component.kind == 'retriever':
1213
+ return sdk_synthetic_ranker(component, reranker=False)
1214
+ if component.kind == 'reranker':
1215
+ return sdk_synthetic_ranker(component, reranker=True)
1216
+ if component.kind == 'fusion':
1217
+ return sdk_synthetic_fusion(component)
1218
+ if component.kind == 'dedup':
1219
+ return sdk_synthetic_dedup(component)
1220
+ if component.kind == 'gate':
1221
+ return sdk_synthetic_gate(component)
1222
+ if component.kind == 'router':
1223
+ return sdk_synthetic_router(component)
1224
+ if component.kind == 'rewriter':
1225
+ return sdk_synthetic_rewriter(component)
1226
+ if component.kind == 'generator':
1227
+ return sdk_synthetic_generator(component)
1228
+ return sdk_synthetic_full_rag(component)
1229
+
1230
+ def synthetic_implementation_sha256() -> str:
1231
+ """Hash the exact source artifact implementing every built-in synthetic adapter."""
1232
+ return hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
1233
+
1234
+ def make_synthetic_adapter(component: ComponentSpec) -> FunctionAdapter:
1235
+ """Create the deterministic, dependency-free built-in for one manifest component."""
1236
+ expected_id = SYNTHETIC_IMPLEMENTATION_IDS[component.kind]
1237
+ if component.implementation.implementation_id != expected_id:
1238
+ raise AdapterRegistrationError(f'synthetic {component.kind} requires implementation ID {expected_id}')
1239
+ if component.implementation.artifact_sha256 != synthetic_implementation_sha256():
1240
+ raise AdapterRegistrationError(f'synthetic {component.kind} artifact hash does not match component_sdk.py')
1241
+ descriptor = AdapterDescriptor.from_component(component)
1242
+ operation = OPERATIONS_BY_KIND[component.kind][0]
1243
+ return FunctionAdapter(descriptor, {operation: sdk_synthetic_handler(component)})
1244
+
1245
+ def make_synthetic_pipeline_adapters(components: Sequence[ComponentSpec]) -> tuple[FunctionAdapter, ...]:
1246
+ """Build a complete Level 2 synthetic adapter tuple in production stage order."""
1247
+ by_kind = {component.kind: component for component in components}
1248
+ if len(by_kind) != len(components):
1249
+ raise AdapterRegistrationError('synthetic pipeline component kinds must be unique')
1250
+ missing = [kind for kind in PIPELINE_COMPONENT_ORDER if kind not in by_kind]
1251
+ if missing:
1252
+ raise AdapterRegistrationError(f"synthetic pipeline is missing component kinds: {', '.join(missing)}")
1253
+ target = EvaluationTarget('level2', 'pipeline')
1254
+ result: list[FunctionAdapter] = []
1255
+ for kind in PIPELINE_COMPONENT_ORDER:
1256
+ component = by_kind[kind]
1257
+ if target not in component.capabilities:
1258
+ raise AdapterRegistrationError(f'component {component.component_id} lacks level2/pipeline capability')
1259
+ result.append(make_synthetic_adapter(component))
1260
+ return tuple(result)
1261
+
1262
+ def make_synthetic_full_rag_adapter(component: ComponentSpec) -> FunctionAdapter:
1263
+ """Build a dependency-free full-RAG subject fixture adapter."""
1264
+ if component.kind != 'full_rag_subject':
1265
+ raise AdapterRegistrationError('full-RAG factory requires a full_rag_subject component')
1266
+ return make_synthetic_adapter(component)
1267
+
1268
+ def make_synthetic_registry(manifest: ComponentManifest) -> ComponentRegistry:
1269
+ """Register every builtin manifest entry; recorded/command entries stay explicit."""
1270
+ registry = ComponentRegistry(manifest)
1271
+ for component in manifest.components:
1272
+ if component.implementation.kind == 'builtin':
1273
+ registry.register(make_synthetic_adapter(component))
1274
+ return registry
1275
+
1276
+ # Strict file loaders are shared with capture reopening.
1277
+ def load_component_manifest(path):
1278
+ from ._track_capture import strict_json
1279
+ return validate_component_manifest(strict_json(Path(path).read_bytes()))
1280
+
1281
+
1282
+ def load_experiment_manifest(path, component_manifest):
1283
+ from ._track_capture import strict_json
1284
+ return validate_experiment_manifest(strict_json(Path(path).read_bytes()), component_manifest)
1285
+
1286
+ """Bounded JSON command adapter for explicitly trusted local RAG components.
1287
+
1288
+ Command adapters execute trusted, pinned local processes. This protocol bounds their
1289
+ wire I/O, root-process waits, and local I/O-thread cleanup. OS-level network,
1290
+ filesystem, and descendant-process containment remains the caller/deployment boundary;
1291
+ use a job object, cgroup, or equivalent when commands are not fully trusted. Default
1292
+ synthetic and recorded evaluation runs launch no processes.
1293
+ """
1294
+ import hashlib
1295
+ import json
1296
+ import os
1297
+ import re
1298
+ import subprocess
1299
+ import threading
1300
+ import time
1301
+ from collections.abc import Mapping, Sequence
1302
+ from contextlib import suppress
1303
+ from dataclasses import dataclass, field
1304
+ from io import BufferedReader
1305
+ from pathlib import Path
1306
+ from typing import IO, Any, cast
1307
+ REQUEST_SCHEMA = 'sable.rag.component-command.request.v1'
1308
+ RESPONSE_SCHEMA = 'sable.rag.component-command.response.v1'
1309
+ MAX_REQUEST_BYTES = 8388608
1310
+ MAX_RESPONSE_BYTES = 33554432
1311
+ MAX_STDERR_BYTES = 65536
1312
+ MAX_STDERR_CHARS = 4096
1313
+ command_PIPE_CHUNK_BYTES = 65536
1314
+ command_TERMINATE_GRACE_SECONDS = 0.25
1315
+ command_KILL_GRACE_SECONDS = 0.25
1316
+ command_THREAD_JOIN_GRACE_SECONDS = 0.25
1317
+ command_ID = re.compile('[a-z0-9][a-z0-9._:-]{0,127}')
1318
+ command_SHA256 = re.compile('[0-9a-f]{64}')
1319
+ command_INTERPRETER = re.compile('(?:pythonw?|node|ruby|perl|php|bash|sh|pwsh|powershell|cmd|java|dotnet)(?:\\d+(?:\\.\\d+)*)?(?:\\.exe)?', re.IGNORECASE)
1320
+ command_INLINE_OR_DISCOVERY_ARGUMENTS = frozenset({'-c', '-e', '-m', '--eval', '--execute'})
1321
+ command_PATH_SUFFIXES = frozenset({'.bat', '.bin', '.cfg', '.cjs', '.cmd', '.com', '.dll', '.dylib', '.exe', '.ini', '.jar', '.js', '.json', '.mjs', '.onnx', '.php', '.pl', '.ps1', '.pt', '.pth', '.py', '.pyw', '.rb', '.safetensors', '.sh', '.so', '.toml', '.ts', '.tsx', '.yaml', '.yml'})
1322
+
1323
+ class ComponentInfrastructureError(RuntimeError):
1324
+ """A command/process failure that must never become a quality score."""
1325
+
1326
+ def __init__(self, code: str, message: str) -> None:
1327
+ self.code = code
1328
+ super().__init__(message)
1329
+
1330
+ @dataclass
1331
+ class _Capture:
1332
+ limit: int
1333
+ data: bytearray = field(default_factory=bytearray)
1334
+ overflow: bool = False
1335
+ error: OSError | None = None
1336
+
1337
+ @dataclass
1338
+ class _Write:
1339
+ error: OSError | None = None
1340
+
1341
+ def command_safe_environment() -> dict[str, str]:
1342
+ allowed = ('COMSPEC', 'LANG', 'PATH', 'PATHEXT', 'SYSTEMROOT', 'TEMP', 'TMP', 'WINDIR')
1343
+ environment = {name: os.environ[name] for name in allowed if name in os.environ}
1344
+ environment['PYTHONIOENCODING'] = 'utf-8'
1345
+ return environment
1346
+
1347
+ def command_normalize(value: object, context: str) -> JsonValue:
1348
+ try:
1349
+ encoded = canonical_json_bytes(cast('JsonValue', value))
1350
+ loaded: Any = json.loads(encoded)
1351
+ except (TypeError, ValueError) as error:
1352
+ raise ComponentInfrastructureError('invalid_payload', f'{context} must contain finite JSON values') from error
1353
+ return cast('JsonValue', loaded)
1354
+
1355
+ def command_looks_like_relative_path(argument: str) -> bool:
1356
+ path = Path(argument)
1357
+ return bool(path.drive or '/' in argument or '\\' in argument or (path.suffix.lower() in command_PATH_SUFFIXES))
1358
+
1359
+ def normalize_component_command(command: Sequence[str]) -> tuple[str, ...]:
1360
+ """Validate bounded argv without resolving executables or relative code at runtime."""
1361
+ result = tuple(command)
1362
+ if not result or len(result) > 32 or (not Path(result[0]).is_absolute()):
1363
+ raise ComponentInfrastructureError('invalid_command', 'component command must be bounded absolute argv')
1364
+ if any((not item or len(item) > 4096 or '\x00' in item for item in result)):
1365
+ raise ComponentInfrastructureError('invalid_command', 'component argv item is invalid')
1366
+ executable = Path(result[0])
1367
+ if not executable.is_file():
1368
+ raise ComponentInfrastructureError('invalid_command', 'component executable must be an existing absolute file')
1369
+ if command_INTERPRETER.fullmatch(executable.name) and (len(result) < 2 or not Path(result[1]).is_absolute() or (not Path(result[1]).is_file())):
1370
+ raise ComponentInfrastructureError('invalid_command', 'interpreter commands require an existing absolute script as argv[1]')
1371
+ if any((argument in command_INLINE_OR_DISCOVERY_ARGUMENTS for argument in result[1:])):
1372
+ raise ComponentInfrastructureError('invalid_command', 'inline code and module discovery command forms are forbidden')
1373
+ for argument in result[1:]:
1374
+ path = Path(argument)
1375
+ if path.is_absolute() and (not path.is_file()):
1376
+ raise ComponentInfrastructureError('invalid_command', 'absolute command path arguments must be existing files')
1377
+ if not path.is_absolute() and command_looks_like_relative_path(argument):
1378
+ raise ComponentInfrastructureError('invalid_command', 'relative command file and code paths are forbidden')
1379
+ return result
1380
+
1381
+ def command_read_bounded(stream: IO[bytes], capture: _Capture, changed: threading.Event) -> None:
1382
+ """Drain one pipe while retaining no more than its configured byte limit."""
1383
+ try:
1384
+ while True:
1385
+ read_size = min(command_PIPE_CHUNK_BYTES, capture.limit - len(capture.data) + 1)
1386
+ chunk = stream.read1(read_size) if isinstance(stream, BufferedReader) else stream.read(read_size)
1387
+ if not chunk:
1388
+ return
1389
+ remaining = capture.limit - len(capture.data)
1390
+ if len(chunk) > remaining:
1391
+ if remaining > 0:
1392
+ capture.data.extend(chunk[:remaining])
1393
+ capture.overflow = True
1394
+ changed.set()
1395
+ return
1396
+ capture.data.extend(chunk)
1397
+ except OSError as error:
1398
+ capture.error = error
1399
+ changed.set()
1400
+ finally:
1401
+ with suppress(OSError):
1402
+ stream.close()
1403
+ changed.set()
1404
+
1405
+ def command_write_request(stream: IO[bytes], wire: bytes, state: _Write, changed: threading.Event) -> None:
1406
+ try:
1407
+ stream.write(wire)
1408
+ stream.flush()
1409
+ except BrokenPipeError:
1410
+ pass
1411
+ except OSError as error:
1412
+ state.error = error
1413
+ changed.set()
1414
+ finally:
1415
+ with suppress(OSError):
1416
+ stream.close()
1417
+ changed.set()
1418
+
1419
+ def command_wait_bounded(process: subprocess.Popen[bytes], timeout: float) -> bool:
1420
+ try:
1421
+ process.wait(timeout=timeout)
1422
+ except subprocess.TimeoutExpired:
1423
+ return False
1424
+ return True
1425
+
1426
+ def command_terminate_and_reap(process: subprocess.Popen[bytes]) -> bool:
1427
+ """Stop and reap a component using only explicitly bounded waits."""
1428
+ if process.poll() is not None:
1429
+ return command_wait_bounded(process, 0)
1430
+ with suppress(OSError):
1431
+ process.terminate()
1432
+ if command_wait_bounded(process, command_TERMINATE_GRACE_SECONDS):
1433
+ return True
1434
+ with suppress(OSError):
1435
+ process.kill()
1436
+ return command_wait_bounded(process, command_KILL_GRACE_SECONDS)
1437
+
1438
+ def command_join_threads_bounded(threads: Sequence[threading.Thread], timeout: float) -> bool:
1439
+ deadline = time.monotonic() + timeout
1440
+ for thread in threads:
1441
+ remaining = deadline - time.monotonic()
1442
+ if remaining <= 0:
1443
+ break
1444
+ thread.join(timeout=remaining)
1445
+ return all((not thread.is_alive() for thread in threads))
1446
+
1447
+ def command_capture_process(argv: tuple[str, ...], wire: bytes, timeout_seconds: float) -> tuple[int, bytes, bytes]:
1448
+ try:
1449
+ executable = Path(argv[0])
1450
+ working_directory = Path(argv[1]).parent if command_INTERPRETER.fullmatch(executable.name) else executable.parent
1451
+ process = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, close_fds=True, cwd=working_directory, env=command_safe_environment())
1452
+ except OSError as error:
1453
+ raise ComponentInfrastructureError('spawn_failed', f'could not start component: {error}') from error
1454
+ if process.stdin is None or process.stdout is None or process.stderr is None:
1455
+ command_terminate_and_reap(process)
1456
+ raise ComponentInfrastructureError('spawn_failed', 'component pipes were not created')
1457
+ changed = threading.Event()
1458
+ stdout = _Capture(MAX_RESPONSE_BYTES)
1459
+ stderr = _Capture(MAX_STDERR_BYTES)
1460
+ writer = _Write()
1461
+ threads = (threading.Thread(target=command_read_bounded, args=(process.stdout, stdout, changed), name='component-stdout', daemon=True), threading.Thread(target=command_read_bounded, args=(process.stderr, stderr, changed), name='component-stderr', daemon=True), threading.Thread(target=command_write_request, args=(process.stdin, wire, writer, changed), name='component-stdin', daemon=True))
1462
+ started: list[threading.Thread] = []
1463
+ try:
1464
+ for thread in threads:
1465
+ thread.start()
1466
+ started.append(thread)
1467
+ except RuntimeError as error:
1468
+ reaped = command_terminate_and_reap(process)
1469
+ joined = command_join_threads_bounded(started, command_THREAD_JOIN_GRACE_SECONDS)
1470
+ cleanup = '' if reaped and joined else '; bounded cleanup was incomplete'
1471
+ raise ComponentInfrastructureError('thread_start_failed', f'could not start component I/O thread{cleanup}') from error
1472
+ deadline = time.monotonic() + timeout_seconds
1473
+ failure: tuple[str, str] | None = None
1474
+ while True:
1475
+ if stdout.overflow:
1476
+ failure = ('response_too_large', 'component response exceeds limit')
1477
+ break
1478
+ if stderr.overflow:
1479
+ failure = ('stderr_too_large', 'component stderr exceeds limit')
1480
+ break
1481
+ if stdout.error is not None or stderr.error is not None or writer.error is not None:
1482
+ failure = ('io_failed', 'component pipe I/O failed')
1483
+ break
1484
+ process_done = process.poll() is not None
1485
+ io_done = all((not thread.is_alive() for thread in threads))
1486
+ if process_done and io_done:
1487
+ break
1488
+ remaining = deadline - time.monotonic()
1489
+ if remaining <= 0:
1490
+ failure = ('timeout', f'component process or pipe I/O timed out after {timeout_seconds:g} seconds')
1491
+ break
1492
+ changed.wait(min(remaining, 0.05))
1493
+ changed.clear()
1494
+ if failure is not None:
1495
+ reaped = command_terminate_and_reap(process)
1496
+ joined = command_join_threads_bounded(threads, command_THREAD_JOIN_GRACE_SECONDS)
1497
+ cleanup = '' if reaped and joined else '; bounded cleanup was incomplete'
1498
+ raise ComponentInfrastructureError(failure[0], failure[1] + cleanup)
1499
+ returncode = process.returncode
1500
+ if returncode is None:
1501
+ command_terminate_and_reap(process)
1502
+ raise ComponentInfrastructureError('process_state', 'component exit state is unavailable')
1503
+ if stdout.overflow:
1504
+ raise ComponentInfrastructureError('response_too_large', 'component response exceeds limit')
1505
+ if stderr.overflow:
1506
+ raise ComponentInfrastructureError('stderr_too_large', 'component stderr exceeds limit')
1507
+ io_error = stdout.error or stderr.error or writer.error
1508
+ if io_error is not None:
1509
+ raise ComponentInfrastructureError('io_failed', f'component pipe I/O failed: {io_error}') from io_error
1510
+ return (returncode, bytes(stdout.data), bytes(stderr.data))
1511
+
1512
+ def run_component_command(command: Sequence[str], *, component_id: str, component_sha256: str, operation: str, payload: Mapping[str, object], timeout_seconds: float) -> dict[str, JsonValue]:
1513
+ """Invoke exactly one pinned component command without shell, discovery, or retry."""
1514
+ argv = normalize_component_command(command)
1515
+ if command_ID.fullmatch(component_id) is None or command_ID.fullmatch(operation) is None:
1516
+ raise ComponentInfrastructureError('invalid_request', 'component or operation ID is invalid')
1517
+ if command_SHA256.fullmatch(component_sha256) is None:
1518
+ raise ComponentInfrastructureError('invalid_request', 'component SHA-256 is invalid')
1519
+ if not 0 < timeout_seconds <= 3600:
1520
+ raise ComponentInfrastructureError('invalid_timeout', 'timeout must be in (0, 3600]')
1521
+ normalized = command_normalize(payload, 'payload')
1522
+ if not isinstance(normalized, dict):
1523
+ raise ComponentInfrastructureError('invalid_payload', 'payload must be an object')
1524
+ request_id = hashlib.sha256(canonical_json_bytes({'component_id': component_id, 'component_sha256': component_sha256, 'operation': operation, 'payload': normalized})).hexdigest()
1525
+ request: dict[str, JsonValue] = {'schema_version': REQUEST_SCHEMA, 'request_id': request_id, 'component_id': component_id, 'component_sha256': component_sha256, 'operation': operation, 'payload': normalized}
1526
+ wire = canonical_json_bytes(request) + b'\n'
1527
+ if len(wire) > MAX_REQUEST_BYTES:
1528
+ raise ComponentInfrastructureError('request_too_large', 'component request exceeds limit')
1529
+ returncode, stdout, stderr = command_capture_process(argv, wire, timeout_seconds)
1530
+ if returncode != 0:
1531
+ stderr_text = stderr.decode('utf-8', errors='replace').strip()[:MAX_STDERR_CHARS]
1532
+ suffix = f': {stderr_text}' if stderr_text else ''
1533
+ raise ComponentInfrastructureError('nonzero_exit', f'component exited {returncode}{suffix}')
1534
+ try:
1535
+ raw: Any = json.loads(stdout.decode('utf-8'), parse_constant=command_reject_constant)
1536
+ except (UnicodeDecodeError, json.JSONDecodeError, ComponentInfrastructureError) as error:
1537
+ raise ComponentInfrastructureError('malformed_json', f'component returned invalid JSON: {error}') from error
1538
+ if not isinstance(raw, Mapping):
1539
+ raise ComponentInfrastructureError('invalid_response', 'component response must be an object')
1540
+ response = cast('Mapping[str, object]', raw)
1541
+ required = {'schema_version', 'request_id', 'component_id', 'component_sha256', 'status', 'output', 'error'}
1542
+ if response.keys() != required or response['schema_version'] != RESPONSE_SCHEMA:
1543
+ raise ComponentInfrastructureError('invalid_response', 'component response contract differs')
1544
+ for key, expected in {'request_id': request_id, 'component_id': component_id, 'component_sha256': component_sha256}.items():
1545
+ if response[key] != expected:
1546
+ raise ComponentInfrastructureError('identity_drift', f'component response changed {key}')
1547
+ if response['status'] != 'ok' or response['error'] is not None:
1548
+ raise ComponentInfrastructureError('component_error', f"component reported error: {response['error']}")
1549
+ output = command_normalize(response['output'], 'response.output')
1550
+ if not isinstance(output, dict):
1551
+ raise ComponentInfrastructureError('invalid_response', 'component output must be an object')
1552
+ return output
1553
+
1554
+ def command_reject_constant(value: str) -> None:
1555
+ raise ComponentInfrastructureError('malformed_json', f'non-finite JSON constant: {value}')
1556
+
1557
+ """Manifest-bound adapter for bounded local component commands."""
1558
+ import hashlib
1559
+ import os
1560
+ import stat
1561
+ from collections.abc import Mapping, Sequence
1562
+ from pathlib import Path
1563
+ from typing import BinaryIO
1564
+ MAX_COMMAND_ARTIFACT_BYTES = 1073741824
1565
+ adapter_HASH_CHUNK_BYTES = 1048576
1566
+
1567
+ def adapter_file_identity(stream: BinaryIO, path: Path) -> tuple[str, int]:
1568
+ before = os.fstat(stream.fileno())
1569
+ if not stat.S_ISREG(before.st_mode):
1570
+ raise AdapterRegistrationError(f'command artifact is not a regular file: {path}')
1571
+ if before.st_size > MAX_COMMAND_ARTIFACT_BYTES:
1572
+ raise AdapterRegistrationError(f'command artifact exceeds byte limit: {path}')
1573
+ digest = hashlib.sha256()
1574
+ while (chunk := stream.read(adapter_HASH_CHUNK_BYTES)):
1575
+ digest.update(chunk)
1576
+ after = os.fstat(stream.fileno())
1577
+ stable_fields = ('st_dev', 'st_ino', 'st_size', 'st_mtime_ns')
1578
+ if any((getattr(before, field) != getattr(after, field) for field in stable_fields)):
1579
+ raise AdapterRegistrationError(f'command artifact changed while hashing: {path}')
1580
+ return (digest.hexdigest(), before.st_size)
1581
+
1582
+ def command_artifact_sha256(command: Sequence[str]) -> str:
1583
+ """Bind validated exact argv plus every absolute regular-file argument."""
1584
+ try:
1585
+ argv = normalize_component_command(command)
1586
+ except ComponentInfrastructureError as error:
1587
+ raise AdapterRegistrationError(str(error)) from error
1588
+ entries: list[JsonValue] = []
1589
+ for argument in argv:
1590
+ path = Path(argument)
1591
+ entry: dict[str, JsonValue] = {'argument': argument}
1592
+ if path.is_absolute():
1593
+ try:
1594
+ with path.open('rb') as stream:
1595
+ file_sha256, size_bytes = adapter_file_identity(stream, path)
1596
+ except OSError as error:
1597
+ raise AdapterRegistrationError(f'cannot hash command artifact {path}') from error
1598
+ entry['file_sha256'] = file_sha256
1599
+ entry['size_bytes'] = size_bytes
1600
+ entries.append(entry)
1601
+ return json_sha256(entries)
1602
+
1603
+ def adapter_component_identity_sha256(descriptor: AdapterDescriptor) -> str:
1604
+ """Bind command requests to the complete runtime-visible component identity."""
1605
+ value: dict[str, JsonValue] = {'capabilities': [{'level': capability.level, 'track': capability.track} for capability in descriptor.capabilities], 'component_id': descriptor.component_id, 'component_kind': descriptor.component_kind, 'component_version': descriptor.component_version, 'config_sha256': descriptor.config_sha256, 'implementation_artifact_sha256': descriptor.implementation_artifact_sha256, 'implementation_id': descriptor.implementation_id, 'implementation_kind': descriptor.implementation_kind, 'implementation_version': descriptor.implementation_version, 'implementation_argv': list(descriptor.implementation_argv), 'operations': list(descriptor.operations), 'resource_context': {'hardware': descriptor.resource_context.hardware, 'device': descriptor.resource_context.device, 'precision': descriptor.resource_context.precision, 'batch_size': descriptor.resource_context.batch_size, 'candidate_depth': descriptor.resource_context.candidate_depth, 'warmup_runs': descriptor.resource_context.warmup_runs, 'measurement_method': descriptor.resource_context.measurement_method, 'timeout_seconds': descriptor.resource_context.timeout_seconds}}
1606
+ return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
1607
+
1608
+ class CommandAdapter:
1609
+ """Invoke one explicitly pinned executable through the bounded JSON protocol."""
1610
+ __slots__ = ('_argv', '_component_sha256', '_descriptor', '_timeout_seconds')
1611
+
1612
+ def __init__(self, component: ComponentSpec, *, timeout_seconds: float | None=None) -> None:
1613
+ if component.implementation.kind != 'command':
1614
+ raise AdapterRegistrationError('CommandAdapter requires a command implementation')
1615
+ if not component.implementation.argv:
1616
+ raise AdapterRegistrationError('CommandAdapter requires pinned absolute argv')
1617
+ timeout = component.resource_context.timeout_seconds if timeout_seconds is None else timeout_seconds
1618
+ if not 0 < timeout <= 3600:
1619
+ raise AdapterRegistrationError('command timeout must be in (0, 3600]')
1620
+ if timeout > component.resource_context.timeout_seconds:
1621
+ raise AdapterRegistrationError(f'command timeout override cannot exceed the component manifest timeout ({component.resource_context.timeout_seconds:g} seconds)')
1622
+ self._descriptor = AdapterDescriptor.from_component(component)
1623
+ self._argv = component.implementation.argv
1624
+ if command_artifact_sha256(self._argv) != component.implementation.artifact_sha256:
1625
+ raise AdapterRegistrationError('command files and argv do not match the implementation artifact hash')
1626
+ self._component_sha256 = adapter_component_identity_sha256(self._descriptor)
1627
+ self._timeout_seconds = timeout
1628
+
1629
+ @property
1630
+ def descriptor(self) -> AdapterDescriptor:
1631
+ return self._descriptor
1632
+
1633
+ @property
1634
+ def capabilities(self) -> tuple[EvaluationTarget, ...]:
1635
+ return self._descriptor.capabilities
1636
+
1637
+ def invoke(self, operation: str, payload: Mapping[str, object]) -> dict[str, JsonValue]:
1638
+ if operation not in self._descriptor.operations:
1639
+ raise AdapterInvocationError(f'operation {operation!r} is not supported by {self._descriptor.component_id}')
1640
+ try:
1641
+ actual_artifact = command_artifact_sha256(self._argv)
1642
+ except AdapterRegistrationError as error:
1643
+ raise ComponentInfrastructureError('identity_drift', str(error)) from error
1644
+ if actual_artifact != self._descriptor.implementation_artifact_sha256:
1645
+ raise ComponentInfrastructureError('identity_drift', 'command files or argv changed after registration')
1646
+ return run_component_command(self._argv, component_id=self._descriptor.component_id, component_sha256=self._component_sha256, operation=operation, payload=payload, timeout_seconds=self._timeout_seconds)
1647
+
1648
+ def make_command_adapter(component: ComponentSpec, *, timeout_seconds: float | None=None) -> CommandAdapter:
1649
+ """Create a command adapter without performing discovery or execution."""
1650
+ return CommandAdapter(component, timeout_seconds=timeout_seconds)
1651
+
1652
+
1653
+ # Equivalent canonical names for the native RAG coordinator. These aliases do
1654
+ # not expand its execution scope or change any manifest field/schema/hash.
1655
+ # Keep aliases after the final loader definition so strict native parsing is
1656
+ # retained when a legacy definition is superseded within this module.
1657
+ BatchConfiguration = ExperimentConfiguration
1658
+ BatchAlternative = ExperimentAlternative
1659
+ BatchPlan = ExperimentManifest
1660
+ validate_batch_manifest = validate_experiment_manifest
1661
+ load_batch_manifest = load_experiment_manifest