equiframe-benchmark 0.12.4__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 (62) hide show
  1. equiframe_benchmark/PROTOCOL_V2.md +172 -0
  2. equiframe_benchmark/__init__.py +284 -0
  3. equiframe_benchmark/__main__.py +3 -0
  4. equiframe_benchmark/_scale_workload.py +375 -0
  5. equiframe_benchmark/calibration_types.py +112 -0
  6. equiframe_benchmark/cli.py +196 -0
  7. equiframe_benchmark/commands.py +149 -0
  8. equiframe_benchmark/commands_scale.py +38 -0
  9. equiframe_benchmark/commands_step10.py +275 -0
  10. equiframe_benchmark/commands_v2.py +206 -0
  11. equiframe_benchmark/cost_table.py +62 -0
  12. equiframe_benchmark/costs.py +788 -0
  13. equiframe_benchmark/distribution.py +932 -0
  14. equiframe_benchmark/evaluation.py +614 -0
  15. equiframe_benchmark/evaluation_v2.py +428 -0
  16. equiframe_benchmark/fixtures.py +299 -0
  17. equiframe_benchmark/freeze.py +869 -0
  18. equiframe_benchmark/freeze_v2.py +412 -0
  19. equiframe_benchmark/integration.py +655 -0
  20. equiframe_benchmark/io.py +29 -0
  21. equiframe_benchmark/models.py +313 -0
  22. equiframe_benchmark/models_v2.py +13 -0
  23. equiframe_benchmark/mutations.py +34 -0
  24. equiframe_benchmark/oracle.py +264 -0
  25. equiframe_benchmark/pairwise.py +248 -0
  26. equiframe_benchmark/pilot.py +254 -0
  27. equiframe_benchmark/py.typed +0 -0
  28. equiframe_benchmark/reconciliation.py +294 -0
  29. equiframe_benchmark/reproduction.py +542 -0
  30. equiframe_benchmark/scale_io.py +19 -0
  31. equiframe_benchmark/scale_process.py +1077 -0
  32. equiframe_benchmark/scale_types.py +120 -0
  33. equiframe_benchmark/scaling.py +529 -0
  34. equiframe_benchmark/schemas/protocol-v2/CommonSupportV2-2.0.schema.json +327 -0
  35. equiframe_benchmark/schemas/protocol-v2/ContrastSpecV2-2.0.schema.json +37 -0
  36. equiframe_benchmark/schemas/protocol-v2/EvaluationConfigV2-2.0.schema.json +184 -0
  37. equiframe_benchmark/schemas/protocol-v2/EvaluationReportV2-2.0.schema.json +1237 -0
  38. equiframe_benchmark/schemas/protocol-v2/EvaluationStratumV2-2.0.schema.json +747 -0
  39. equiframe_benchmark/schemas/protocol-v2/FreezeDefinitionV2-2.0.schema.json +5951 -0
  40. equiframe_benchmark/schemas/protocol-v2/FreezeManifestV2-2.0.schema.json +6222 -0
  41. equiframe_benchmark/schemas/protocol-v2/PrincipalContrastV2-2.0.schema.json +578 -0
  42. equiframe_benchmark/schemas/protocol-v2/ProtocolReviewV2-2.0.schema.json +56 -0
  43. equiframe_benchmark/schemas/protocol-v2/SignedMetricV2-2.0.schema.json +51 -0
  44. equiframe_benchmark/schemas/protocol-v2/StudyCaseResultV2-2.0.schema.json +4881 -0
  45. equiframe_benchmark/schemas/protocol-v2/StudyResultV2-2.0.schema.json +8370 -0
  46. equiframe_benchmark/schemas/protocol-v2/TableExportV2-2.0.schema.json +75 -0
  47. equiframe_benchmark/selection.py +178 -0
  48. equiframe_benchmark/snapshot.py +24 -0
  49. equiframe_benchmark/splits.py +92 -0
  50. equiframe_benchmark/study.py +526 -0
  51. equiframe_benchmark/study_bundle.py +379 -0
  52. equiframe_benchmark/study_bundle_v2.py +388 -0
  53. equiframe_benchmark/study_v2.py +544 -0
  54. equiframe_benchmark/tables.py +347 -0
  55. equiframe_benchmark/tables_v2.py +340 -0
  56. equiframe_benchmark/training_types.py +52 -0
  57. equiframe_benchmark-0.12.4.dist-info/METADATA +343 -0
  58. equiframe_benchmark-0.12.4.dist-info/RECORD +62 -0
  59. equiframe_benchmark-0.12.4.dist-info/WHEEL +4 -0
  60. equiframe_benchmark-0.12.4.dist-info/entry_points.txt +3 -0
  61. equiframe_benchmark-0.12.4.dist-info/licenses/LICENSE +21 -0
  62. equiframe_benchmark-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +8 -0
@@ -0,0 +1,375 @@
1
+ """Trusted isolated synthetic cost worker. No source/service/network access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ import shutil
8
+ import sys
9
+ import time
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+ from typing import Any, Literal, TypeVar
13
+
14
+ import equiframe as ef
15
+ from equiframe.evidence import describe_file, inventory, load_json, publish_json, verify_files
16
+
17
+ from equiframe_benchmark.costs import _Monitor, _PsutilProbe
18
+ from equiframe_benchmark.models import digest
19
+ from equiframe_benchmark.scale_io import save_scale_document
20
+ from equiframe_benchmark.scale_types import (
21
+ SCALE_SAMPLE_INTERVAL_SECONDS,
22
+ BaselineCost,
23
+ ScaleSpec,
24
+ ScaleTrial,
25
+ ScaleWorkerCompletion,
26
+ StageCost,
27
+ )
28
+
29
+ _T = TypeVar("_T")
30
+
31
+
32
+ def make_contract(spec: ScaleSpec) -> ef.Contract:
33
+ columns = tuple(
34
+ ef.ColumnSpec(
35
+ name="id" if i == 0 else f"value_{i}",
36
+ logical_type=ef.StringType()
37
+ if i > 1 and i % 2 == 0
38
+ else ef.IntegerType(bits=64, signed=True),
39
+ nullable=i > 0 and spec.null_every is not None,
40
+ numeric_policy=None
41
+ if i > 1 and i % 2 == 0
42
+ else ef.NumericPolicy(comparison="exact", unit="count"),
43
+ null_policy=ef.NullPolicy(representations=("null",)),
44
+ )
45
+ for i in range(spec.width)
46
+ )
47
+ keys = (
48
+ (
49
+ ef.KeySpec(
50
+ name="id",
51
+ columns=("id",),
52
+ unique=True,
53
+ incomplete="reject",
54
+ scope=ef.TableKeyScope(),
55
+ ),
56
+ )
57
+ if spec.alignment == "keyed"
58
+ else ()
59
+ )
60
+ schema = ef.TableSchema(
61
+ columns=columns, keys=keys, multiplicity="preserve", order=ef.IgnoredOrder()
62
+ )
63
+ return ef.Contract(
64
+ schema_version="1.0",
65
+ contract_id="engineering.scale",
66
+ revision=1,
67
+ family="duplicates_order",
68
+ inputs=(ef.TableSpec(name="source", schema_spec=schema),),
69
+ output=ef.TableSpec(name="result", schema_spec=schema),
70
+ preconditions=(),
71
+ rules=(),
72
+ )
73
+
74
+
75
+ def _schema(spec: ScaleSpec) -> Any:
76
+ pa = importlib.import_module("pyarrow")
77
+ return pa.schema(
78
+ [
79
+ pa.field(
80
+ "id" if i == 0 else f"value_{i}",
81
+ pa.string() if i > 1 and i % 2 == 0 else pa.int64(),
82
+ nullable=i > 0 and spec.null_every is not None,
83
+ )
84
+ for i in range(spec.width)
85
+ ]
86
+ )
87
+
88
+
89
+ def _value(spec: ScaleSpec, row: int, column: int, *, changed: bool) -> int | str | None:
90
+ identity = row % (spec.distinct_rows or spec.rows)
91
+ if column == 0:
92
+ return identity
93
+ # Alter the first payload column after NULL generation, so even NULL is a real change.
94
+ if changed and column == 1:
95
+ return -(identity + 1)
96
+ if spec.null_every is not None and identity % spec.null_every == 0:
97
+ return None
98
+ number = 0 if identity % 100 < spec.skew_percent else identity % spec.cardinality
99
+ if column > 1 and column % 2 == 0:
100
+ return ("x" * (1 + number % spec.text_max_length)) + str(number)
101
+ return number
102
+
103
+
104
+ def capture(spec: ScaleSpec, directory: Path) -> tuple[Path, Path]:
105
+ """Generate both complete sides in bounded batches directly into Arrow IPC files."""
106
+ pa = importlib.import_module("pyarrow")
107
+ schema = _schema(spec)
108
+ paths = directory / "candidate.arrow", directory / "expected.arrow"
109
+ for side, path in enumerate(paths):
110
+ with pa.OSFile(str(path), "wb") as sink, pa.ipc.new_file(sink, schema) as writer:
111
+ for start in range(0, spec.rows, spec.batch_rows):
112
+ rows = range(start, min(start + spec.batch_rows, spec.rows))
113
+ arrays = [
114
+ pa.array(
115
+ [
116
+ _value(
117
+ spec,
118
+ row,
119
+ column,
120
+ changed=side == 0
121
+ and (
122
+ (spec.difference_last and row == spec.rows - 1)
123
+ or (
124
+ spec.difference_every is not None
125
+ and row % spec.difference_every == 0
126
+ )
127
+ ),
128
+ )
129
+ for row in rows
130
+ ],
131
+ type=schema.field(column).type,
132
+ )
133
+ for column in range(spec.width)
134
+ ]
135
+ writer.write_batch(pa.RecordBatch.from_arrays(arrays, schema=schema))
136
+ return paths
137
+
138
+
139
+ def _snapshot(paths: tuple[Path, Path], directory: Path) -> None:
140
+ pa = importlib.import_module("pyarrow")
141
+ records = []
142
+ for path in paths:
143
+ with pa.memory_map(str(path), "r") as source:
144
+ reader = pa.ipc.open_file(source)
145
+ rows = sum(reader.get_batch(i).num_rows for i in range(reader.num_record_batches))
146
+ records.append(
147
+ {"artifact": describe_file(path, directory).model_dump(mode="json"), "rows": rows}
148
+ )
149
+ publish_json({"inputs": records}, directory / "snapshot.json")
150
+
151
+
152
+ def _baseline(spec: ScaleSpec, paths: tuple[Path, Path], method: str) -> BaselineCost:
153
+ name: Any = method
154
+ if method == "datacompy":
155
+ return BaselineCost(
156
+ method=name,
157
+ state="not_available",
158
+ wall_seconds=None,
159
+ reason=(
160
+ "DataComPy is not a pinned dependency of this candidate; "
161
+ "no untested semantic or version claims"
162
+ ),
163
+ )
164
+ if method == "pandas_assert" and spec.alignment != "keyed":
165
+ return BaselineCost(
166
+ method=name,
167
+ state="not_applicable",
168
+ wall_seconds=None,
169
+ reason=(
170
+ "Native assertion baseline is restricted to declared unique keys; "
171
+ "no silent row sorting or multiset policy substitution"
172
+ ),
173
+ )
174
+ start = time.perf_counter()
175
+ state: Literal["equivalent", "different"]
176
+ try:
177
+ pa = importlib.import_module("pyarrow")
178
+ engine = importlib.import_module("pandas" if method == "pandas_assert" else "duckdb")
179
+ tables = []
180
+ for path in paths:
181
+ with pa.memory_map(str(path), "r") as source:
182
+ tables.append(pa.ipc.open_file(source).read_all())
183
+ if not tables[0].schema.equals(tables[1].schema, check_metadata=True):
184
+ state = "different"
185
+ elif method == "pandas_assert":
186
+ frames = [
187
+ table.to_pandas(types_mapper=engine.ArrowDtype)
188
+ .sort_values("id")
189
+ .reset_index(drop=True)
190
+ for table in tables
191
+ ]
192
+ try:
193
+ engine.testing.assert_frame_equal(
194
+ frames[0], frames[1], check_exact=True, check_dtype=True, check_names=True
195
+ )
196
+ state = "equivalent"
197
+ except AssertionError:
198
+ state = "different"
199
+ else:
200
+ with engine.connect(config={"threads": "1", "memory_limit": "256MB"}) as connection:
201
+ connection.register("lhs", tables[0])
202
+ connection.register("rhs", tables[1])
203
+ delta = connection.execute(
204
+ "SELECT EXISTS(SELECT * FROM (SELECT * FROM lhs EXCEPT ALL SELECT * FROM rhs) "
205
+ "UNION ALL SELECT * FROM (SELECT * FROM rhs EXCEPT ALL SELECT * FROM lhs))"
206
+ ).fetchone()
207
+ state = "different" if delta[0] else "equivalent"
208
+ return BaselineCost(
209
+ method=name,
210
+ state=state,
211
+ wall_seconds=time.perf_counter() - start,
212
+ reason=(
213
+ "Exact integers/text/NULL and equal Arrow schemas; includes native loading/"
214
+ "conversion/query; excludes full differences, durable snapshots, reporting "
215
+ "and integrity verification"
216
+ ),
217
+ )
218
+ except ImportError:
219
+ return BaselineCost(
220
+ method=name,
221
+ state="not_available",
222
+ wall_seconds=None,
223
+ reason="Optional native engine is not installed",
224
+ )
225
+ except Exception as error:
226
+ return BaselineCost(
227
+ method=name,
228
+ state="failed",
229
+ wall_seconds=time.perf_counter() - start,
230
+ reason=type(error).__name__,
231
+ )
232
+
233
+
234
+ def run_trial(spec: ScaleSpec, directory: Path, repetition: int) -> ScaleTrial:
235
+ directory.mkdir()
236
+ stages: list[StageCost] = []
237
+ monitor = _Monitor(_PsutilProbe, SCALE_SAMPLE_INTERVAL_SECONDS)
238
+ monitor.start()
239
+ start = time.perf_counter()
240
+
241
+ def measured(name: str, scope: str, action: Callable[[], _T]) -> _T:
242
+ began = time.perf_counter()
243
+ value = action()
244
+ stages.append(StageCost(name=name, wall_seconds=time.perf_counter() - began, scope=scope))
245
+ return value
246
+
247
+ try:
248
+ paths = measured(
249
+ "capture",
250
+ "Synthetic generation and batch Arrow IPC writing; no external database",
251
+ lambda: capture(spec, directory),
252
+ )
253
+ measured(
254
+ "snapshot",
255
+ "Complete input hashing and batch row counting",
256
+ lambda: _snapshot(paths, directory),
257
+ )
258
+ transferred = directory / "left.arrow", directory / "right.arrow"
259
+ measured(
260
+ "transfer",
261
+ "Local file copies only, no network transfer",
262
+ lambda: tuple(shutil.copyfile(a, b) for a, b in zip(paths, transferred, strict=True)),
263
+ )
264
+ contract = measured(
265
+ "plan",
266
+ "Declare and validate the exact contract; "
267
+ "comparator internal planning remains in compare",
268
+ lambda: make_contract(spec),
269
+ )
270
+ result = measured(
271
+ "compare",
272
+ "Public compare end-to-end including its input snapshots, planning, "
273
+ "exact SQLite work, serialization and persistence",
274
+ lambda: ef.compare(
275
+ *transferred,
276
+ contract=contract,
277
+ reference="expected",
278
+ work_dir=directory / "comparison",
279
+ options=spec.comparison_options,
280
+ ),
281
+ )
282
+ serialized = measured(
283
+ "serialize",
284
+ "Additional standalone serialization of the returned comparison document",
285
+ lambda: result.model_dump_json().encode("utf-8"),
286
+ )
287
+
288
+ def persist() -> None:
289
+ target = directory / "serialized-comparison.json"
290
+ partial = directory / "serialized-comparison.json.part"
291
+ with partial.open("xb") as output:
292
+ output.write(serialized)
293
+ output.flush()
294
+ os.fsync(output.fileno())
295
+ os.link(partial, target)
296
+ partial.unlink()
297
+
298
+ measured(
299
+ "persist",
300
+ "Additional standalone fsync plus exclusive atomic manifest publication",
301
+ persist,
302
+ )
303
+ measured(
304
+ "render",
305
+ "Privacy-filtered report construction and HTML/text/JSON publication "
306
+ "without reexecuting inputs",
307
+ lambda: ef.write_comparison_report(result, directory / "report"),
308
+ )
309
+
310
+ def verify() -> int:
311
+ restored = ef.read_comparison(directory / "serialized-comparison.json")
312
+ if restored != result:
313
+ raise ValueError("comparison roundtrip changed")
314
+ count = sum(1 for _ in ef.iter_differences(result.differences))
315
+ verify_files(directory, inventory(directory))
316
+ return count
317
+
318
+ count = measured(
319
+ "verify",
320
+ "Comparison reader, all difference records, and complete evidence SHA-256 reread",
321
+ verify,
322
+ )
323
+ elapsed = time.perf_counter() - start
324
+ finally:
325
+ resources = monitor.finish(time.perf_counter() - start)
326
+ # Baselines are separate services and outside EquiFrame pipeline duration/RSS samples.
327
+ baselines = (
328
+ tuple(
329
+ _baseline(spec, transferred, name)
330
+ for name in ("pandas_assert", "duckdb_sql", "datacompy")
331
+ )
332
+ if spec.run_baselines
333
+ else ()
334
+ )
335
+ evidence = inventory(directory)
336
+ return ScaleTrial(
337
+ case_sha256=digest(spec),
338
+ repetition=repetition,
339
+ cache_state="fresh_process_os_cache_uncontrolled"
340
+ if repetition == 0
341
+ else "warm_process_os_cache_uncontrolled",
342
+ stages=tuple(stages),
343
+ end_to_end_seconds=elapsed,
344
+ resources=resources,
345
+ comparison=result,
346
+ verified_difference_records=count,
347
+ evidence_bytes=sum(item.size_bytes for item in evidence),
348
+ evidence=evidence,
349
+ baselines=baselines,
350
+ )
351
+
352
+
353
+ def main() -> None:
354
+ from equiframe_benchmark.scaling import _environment
355
+
356
+ spec = load_json(ScaleSpec, Path(sys.argv[1]))
357
+ directory = Path(sys.argv[2])
358
+ for repetition in range(spec.repetitions):
359
+ trial = run_trial(spec, directory / f"trial-{repetition:03d}", repetition)
360
+ save_scale_document(trial, directory / f"trial-{repetition:03d}.json")
361
+ files = tuple(
362
+ item
363
+ for item in inventory(directory)
364
+ if item.path in {f"trial-{i:03d}.json" for i in range(spec.repetitions)}
365
+ )
366
+ save_scale_document(
367
+ ScaleWorkerCompletion(
368
+ environment=_environment(), case_sha256=digest(spec), trial_files=files
369
+ ),
370
+ directory / "worker-completed.json",
371
+ )
372
+
373
+
374
+ if __name__ == "__main__":
375
+ main()
@@ -0,0 +1,112 @@
1
+ """Reviewed calibration lineage and group support stay outside detector features."""
2
+
3
+ from typing import Literal
4
+
5
+ from equiframe import NormalCalibrationProfiles
6
+ from pydantic import model_validator
7
+ from typing_extensions import Self
8
+
9
+ from equiframe_benchmark.models import Digest, Document, Text, digest
10
+ from equiframe_benchmark.training_types import TrainingRecord, TrainingSelection
11
+
12
+
13
+ class CalibrationManifest(Document):
14
+ purpose: Literal["normal_calibration"] = "normal_calibration"
15
+ catalog_sha256: Digest
16
+ splits_sha256: Digest
17
+ records: tuple[TrainingRecord, ...]
18
+
19
+ @model_validator(mode="after")
20
+ def unique(self) -> Self:
21
+ if not self.records:
22
+ raise ValueError("nonempty normal calibration selection required")
23
+ for field in ("candidate_id", "execution_sha256", "profile_sha256"):
24
+ if len({getattr(r, field) for r in self.records}) != len(self.records):
25
+ raise ValueError("duplicate calibration evidence")
26
+ return self
27
+
28
+
29
+ class CalibrationSelection(Document):
30
+ manifest: CalibrationManifest
31
+ calibration: NormalCalibrationProfiles
32
+
33
+ @model_validator(mode="after")
34
+ def consistent(self) -> Self:
35
+ if digest(self.manifest) != self.calibration.selection_manifest_sha256:
36
+ raise ValueError("calibration manifest mismatch")
37
+ if len(self.manifest.records) != len(self.calibration.profiles):
38
+ raise ValueError("calibration selection size mismatch")
39
+ if any(
40
+ record.vector_sha256 != digest(vector)
41
+ for record, vector in zip(self.manifest.records, self.calibration.profiles, strict=True)
42
+ ):
43
+ raise ValueError("calibration vectors do not match recorded evidence")
44
+ return self
45
+
46
+
47
+ class DetectorSelectionAudit(Document):
48
+ training_sha256: Digest
49
+ calibration_sha256: Digest
50
+ training_groups: int
51
+ calibration_groups: int
52
+ min_training_groups: int
53
+ min_calibration_groups: int
54
+ rationale: Text
55
+ state: Literal["eligible", "not_estimable"]
56
+
57
+ @model_validator(mode="after")
58
+ def consistent(self) -> Self:
59
+ if min(self.min_training_groups, self.min_calibration_groups) < 1:
60
+ raise ValueError("explicit positive group minima required")
61
+ if min(self.training_groups, self.calibration_groups) < 1:
62
+ raise ValueError("selections must contain groups")
63
+ eligible = (
64
+ self.training_groups >= self.min_training_groups
65
+ and self.calibration_groups >= self.min_calibration_groups
66
+ )
67
+ if (self.state == "eligible") != eligible:
68
+ raise ValueError("audit state contradicts group support")
69
+ return self
70
+
71
+
72
+ def audit_detector_selections(
73
+ training: TrainingSelection,
74
+ calibration: CalibrationSelection,
75
+ *,
76
+ min_training_groups: int,
77
+ min_calibration_groups: int,
78
+ rationale: str,
79
+ ) -> DetectorSelectionAudit:
80
+ """Check pair provenance/support before fitting; eligibility is not a power guarantee."""
81
+ training = TrainingSelection.model_validate(training)
82
+ calibration = CalibrationSelection.model_validate(calibration)
83
+ t, c = training.manifest, calibration.manifest
84
+ if t.catalog_sha256 != c.catalog_sha256 or t.splits_sha256 != c.splits_sha256:
85
+ raise ValueError("training and calibration require the same catalog and split revision")
86
+ if (
87
+ len(
88
+ {
89
+ p.spec_sha256
90
+ for p in (*training.training.profiles, *calibration.calibration.profiles)
91
+ }
92
+ )
93
+ != 1
94
+ ):
95
+ raise ValueError("a detector pair requires one family and feature revision")
96
+ for field in ("group_id", "scenario_id", "candidate_id", "execution_sha256", "profile_sha256"):
97
+ if {getattr(r, field) for r in t.records} & {getattr(r, field) for r in c.records}:
98
+ raise ValueError("training/calibration leakage in " + field)
99
+ train_groups = len({r.group_id for r in t.records})
100
+ calibration_groups = len({r.group_id for r in c.records})
101
+ return DetectorSelectionAudit(
102
+ training_sha256=digest(training),
103
+ calibration_sha256=digest(calibration),
104
+ training_groups=train_groups,
105
+ calibration_groups=calibration_groups,
106
+ min_training_groups=min_training_groups,
107
+ min_calibration_groups=min_calibration_groups,
108
+ rationale=rationale,
109
+ state="eligible"
110
+ if train_groups >= min_training_groups and calibration_groups >= min_calibration_groups
111
+ else "not_estimable",
112
+ )
@@ -0,0 +1,196 @@
1
+ """Benchmark commands keep experimental labels outside the operational library."""
2
+
3
+ import importlib
4
+ import json
5
+ import re
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from uuid import uuid4
9
+
10
+ import typer
11
+ from equiframe._cli_support import exit_with_failure
12
+
13
+ from equiframe_benchmark.commands import register as _register_delivery
14
+ from equiframe_benchmark.commands_scale import register as _register_scale
15
+ from equiframe_benchmark.commands_step10 import register as _register_step10
16
+ from equiframe_benchmark.commands_v2 import register_v2 as _register_v2
17
+ from equiframe_benchmark.io import load_document, save_document
18
+ from equiframe_benchmark.models import Catalog, SplitManifest
19
+ from equiframe_benchmark.splits import dependency_groups, make_splits, split_counts, validate_splits
20
+
21
+ app = typer.Typer(no_args_is_help=True, add_completion=False, pretty_exceptions_enable=False)
22
+
23
+
24
+ def _failure(error: Exception, *, input_validation: bool = True) -> None:
25
+ invalid = input_validation and isinstance(error, ValueError)
26
+ exit_with_failure(
27
+ error,
28
+ code="invalid_input" if invalid else "operation_failed",
29
+ exit_code=2 if invalid else 3,
30
+ input_validation=input_validation,
31
+ )
32
+
33
+
34
+ @app.command("validate")
35
+ def validate_command(catalog: Path, splits: Path | None = typer.Option(None)) -> None:
36
+ """Validate a catalog and optional grouped partition manifest; never create labels."""
37
+ try:
38
+ document = load_document(Catalog, catalog)
39
+ summary: dict[str, object] = {
40
+ "catalog_id": document.catalog_id,
41
+ "synthetic": document.synthetic,
42
+ "scenarios": len(document.scenarios),
43
+ "groups": len(dependency_groups(document)),
44
+ "scientific_review_confirmed": False,
45
+ }
46
+ if splits:
47
+ manifest = load_document(SplitManifest, splits)
48
+ validate_splits(document, manifest)
49
+ summary["partitions"] = split_counts(document, manifest)
50
+ except Exception as error:
51
+ _failure(error)
52
+ return
53
+ typer.echo(json.dumps(summary, sort_keys=True))
54
+
55
+
56
+ @app.command("split")
57
+ def split_command(
58
+ catalog: Path,
59
+ output: Path = typer.Option(...),
60
+ seed: int = typer.Option(...),
61
+ training: int = typer.Option(3),
62
+ calibration: int = typer.Option(1),
63
+ test: int = typer.Option(2),
64
+ ) -> None:
65
+ """Create an exclusive grouped split with an explicit seed and integer weights."""
66
+ try:
67
+ document = load_document(Catalog, catalog)
68
+ manifest = make_splits(document, seed=seed, weights=(training, calibration, test))
69
+ validate_splits(document, manifest)
70
+ save_document(manifest, output)
71
+ except Exception as error:
72
+ _failure(error)
73
+ return
74
+ typer.echo(json.dumps(split_counts(document, manifest), sort_keys=True))
75
+
76
+
77
+ @app.command("pilot")
78
+ def pilot_command(output: Path = typer.Option(Path("equiframe-benchmark-runs"))) -> None:
79
+ """Run a new synthetic native pilot; human reviews remain pending."""
80
+ from equiframe_benchmark.pilot import run_pilot
81
+
82
+ directory = output.resolve() / ("pilot_" + uuid4().hex)
83
+ try:
84
+ summary = run_pilot(directory)
85
+ except Exception as error:
86
+ _failure(error)
87
+ return
88
+ typer.echo(json.dumps({"pilot_id": directory.name, **summary}, sort_keys=True))
89
+
90
+
91
+ def _factory(reference: str) -> Callable[[], object]:
92
+ from typing import cast
93
+
94
+ if not re.fullmatch(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*", reference):
95
+ raise ValueError("explicit module:factory reference is required")
96
+ module, name = reference.split(":")
97
+ factory = getattr(importlib.import_module(module), name)
98
+ if not callable(factory):
99
+ raise ValueError("registered factory must be callable")
100
+ return cast(Callable[[], object], factory)
101
+
102
+
103
+ @app.command("integrate")
104
+ def integrate_command(
105
+ spec: Path,
106
+ snapshot_id: str = typer.Option(...),
107
+ client: str = typer.Option(...),
108
+ registry: str = typer.Option(...),
109
+ output: Path = typer.Option(Path("equiframe-integration")),
110
+ expected: Path | None = typer.Option(None),
111
+ bundle: Path | None = typer.Option(None),
112
+ ) -> None:
113
+ """Consume a pinned snapshot through explicitly supplied trusted client/code."""
114
+ from typing import cast
115
+
116
+ import equiframe as ef
117
+ from equiframe.evidence import load_json
118
+
119
+ from equiframe_benchmark.integration import IntegrationSpec, run_integration
120
+ from equiframe_benchmark.snapshot import SnapshotClient, SnapshotDataSource
121
+
122
+ try:
123
+ configuration = load_json(IntegrationSpec, spec)
124
+ detector = load_json(ef.DetectorBundle, bundle) if bundle else None
125
+ registered = _factory(registry)()
126
+ if not isinstance(registered, ef.TransformationRegistry):
127
+ raise ValueError("registry factory must return a TransformationRegistry")
128
+ transport = _factory(client)()
129
+ if not all(
130
+ callable(getattr(transport, name, None))
131
+ for name in ("get_snapshot", "download_artifact")
132
+ ):
133
+ raise ValueError("client factory must expose the snapshot client protocol")
134
+ source = SnapshotDataSource(
135
+ snapshot_id,
136
+ client=cast(SnapshotClient, transport),
137
+ expected_domain_schema_revision=configuration.domain_schema_revision,
138
+ table_names=tuple(
139
+ table.name for table in configuration.pipelines[0].execution.contract.inputs
140
+ ),
141
+ )
142
+ result = run_integration(
143
+ configuration,
144
+ source=source,
145
+ registry=registered,
146
+ detector_bundle=detector,
147
+ expected=expected,
148
+ work_dir=output,
149
+ )
150
+ except Exception as error:
151
+ _failure(error)
152
+ return
153
+ typer.echo(
154
+ json.dumps(
155
+ {
156
+ "integration_id": result.integration_id,
157
+ "complete": result.complete,
158
+ "inputs_verified": result.inputs_verified,
159
+ "scientific_equivalence_confirmed": False,
160
+ },
161
+ sort_keys=True,
162
+ )
163
+ )
164
+ raise typer.Exit(code=result.exit_code)
165
+
166
+
167
+ @app.command("verify-integration")
168
+ def verify_integration_command(directory: Path) -> None:
169
+ """Verify the existing receipt and evidence without contacting the service."""
170
+ from equiframe_benchmark.integration import read_integration
171
+
172
+ try:
173
+ result = read_integration(directory)
174
+ except Exception as error:
175
+ _failure(error, input_validation=False)
176
+ return
177
+ typer.echo(
178
+ json.dumps(
179
+ {
180
+ "integration_id": result.integration_id,
181
+ "evidence_verified": True,
182
+ "complete": result.complete,
183
+ },
184
+ sort_keys=True,
185
+ )
186
+ )
187
+
188
+
189
+ _register_scale(app)
190
+ _register_step10(app)
191
+ _register_v2(app)
192
+ _register_delivery(app)
193
+
194
+
195
+ def main() -> None:
196
+ app()