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,526 @@
1
+ """Run frozen held-out cases without fitting, fabricating labels or dropping failures."""
2
+
3
+ from datetime import datetime
4
+ from datetime import timezone as _timezone
5
+ from pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+ from typing import Literal, cast
8
+ from uuid import uuid4
9
+
10
+ import equiframe as ef
11
+ from equiframe.evidence import inventory, verify_files
12
+ from equiframe.local import local_path
13
+ from pydantic import model_validator
14
+ from typing_extensions import Self
15
+
16
+ from equiframe_benchmark.evaluation import (
17
+ Engine,
18
+ EvaluationConfig,
19
+ EvaluationRecord,
20
+ EvaluationReport,
21
+ evaluate_records,
22
+ )
23
+ from equiframe_benchmark.freeze import (
24
+ FreezeAudit,
25
+ FreezeManifest,
26
+ FrozenFamilyModel,
27
+ TestCaseDefinition,
28
+ audit_freeze,
29
+ )
30
+ from equiframe_benchmark.io import load_document, save_document
31
+ from equiframe_benchmark.models import Digest, Document, Scenario, Text, digest, same_content
32
+ from equiframe_benchmark.reproduction import _bounded_json, _stream_file
33
+ from equiframe_benchmark.tables import TableExport, read_tables, write_tables
34
+
35
+ UTC = _timezone.utc
36
+
37
+
38
+ class StudyCaseResult(Document):
39
+ case_id: Text
40
+ pipeline_directory: Text | None
41
+ pipeline: ef.PipelineResult | None
42
+ output_replay: ef.ComparisonResult | None
43
+ error: Text | None
44
+ record: EvaluationRecord
45
+
46
+ @model_validator(mode="after")
47
+ def consistent(self) -> Self:
48
+ if self.case_id != self.record.case_id:
49
+ raise ValueError("case result must preserve its declared case identity")
50
+ if self.pipeline is None:
51
+ if self.pipeline_directory is not None or self.error is None:
52
+ raise ValueError("missing pipeline requires a recorded operation failure")
53
+ elif self.pipeline_directory != self.pipeline.pipeline_id or self.error is not None:
54
+ raise ValueError("pipeline directory and operation result must agree")
55
+ return self
56
+
57
+
58
+ def _binding_reason(
59
+ case: TestCaseDefinition,
60
+ scenario: Scenario,
61
+ pipeline: ef.PipelineResult | None,
62
+ replay: ef.ComparisonResult | None,
63
+ ) -> str | None:
64
+ execution = pipeline.execution if pipeline is not None else None
65
+ if execution is None or execution.state != "completed" or execution.output is None:
66
+ return "native_execution_unavailable"
67
+ if execution.implementation_version != case.candidate.implementation:
68
+ return "implementation_version_changed"
69
+ supplied = {table.name: table.data for table in execution.inputs}
70
+ if set(supplied) != {item.name for item in scenario.inputs} or any(
71
+ not same_content(item.artifact, supplied[item.name]) for item in scenario.inputs
72
+ ):
73
+ return "scenario_inputs_changed"
74
+ if replay is None or replay.state != "equivalent" or not replay.complete:
75
+ return "reviewed_output_not_reproduced"
76
+ return None
77
+
78
+
79
+ def _unavailable(reason: str) -> ef.ComponentDecision:
80
+ return ef.ComponentDecision(decision="not_evaluable", complete=False, reason=reason)
81
+
82
+
83
+ def _config(freeze: FreezeManifest, after: FreezeAudit) -> EvaluationConfig:
84
+ # Changed sources cannot produce a scientific final report. Preserve the raw
85
+ # failed run as a pilot report with its separately bound original freeze.
86
+ final = freeze.definition.mode == "final" and after == freeze.audit and after.ready
87
+ return EvaluationConfig.model_validate(
88
+ freeze.definition.evaluation.model_copy(
89
+ update={
90
+ "mode": "final" if final else "pilot",
91
+ "freeze_sha256": digest(freeze) if final else None,
92
+ }
93
+ )
94
+ )
95
+
96
+
97
+ def _record(
98
+ case: TestCaseDefinition,
99
+ scenario: Scenario,
100
+ group_id: str,
101
+ model: FrozenFamilyModel,
102
+ pipeline: ef.PipelineResult | None,
103
+ replay: ef.ComparisonResult | None,
104
+ error: str | None,
105
+ config: EvaluationConfig,
106
+ ) -> EvaluationRecord:
107
+ if pipeline is not None and pipeline.spec != case.pipeline:
108
+ raise ValueError("pipeline must use the exact frozen case configuration")
109
+ if pipeline is not None and pipeline.detection_requested != (model.bundle is not None):
110
+ raise ValueError("pipeline detector request must match the frozen family bundle")
111
+ if (
112
+ pipeline is not None
113
+ and pipeline.detection is not None
114
+ and (
115
+ model.bundle is None
116
+ or pipeline.detection.bundle_sha256 != ef.detector_fingerprint(model.bundle)
117
+ )
118
+ ):
119
+ raise ValueError("pipeline detection must use the frozen family bundle")
120
+ execution = pipeline.execution if pipeline is not None else None
121
+ valid_execution = execution is not None and execution.state == "completed"
122
+ reason = _binding_reason(case, scenario, pipeline, replay)
123
+ oracle = case.oracle
124
+ oracle_state = (
125
+ oracle.status
126
+ if oracle is not None and reason is None
127
+ else ("pending" if oracle is None and reason is None else "not_evaluable")
128
+ )
129
+ label = cast(
130
+ Literal["normal", "divergent"] | None,
131
+ oracle.label if oracle is not None and oracle_state == "confirmed" else None,
132
+ )
133
+ failure = error or (
134
+ "native_execution_unavailable"
135
+ if not valid_execution
136
+ else "pipeline_incomplete"
137
+ if pipeline is not None and not pipeline.complete
138
+ else reason
139
+ )
140
+ r = (
141
+ pipeline.hybrid.r
142
+ if valid_execution and pipeline is not None
143
+ else _unavailable(failure or "pipeline_unavailable")
144
+ )
145
+ m = (
146
+ pipeline.hybrid.m
147
+ if valid_execution and pipeline is not None
148
+ else _unavailable(failure or "pipeline_unavailable")
149
+ )
150
+ h = (
151
+ pipeline.hybrid.result
152
+ if valid_execution and pipeline is not None
153
+ else _unavailable(failure or "pipeline_unavailable")
154
+ )
155
+ detection = pipeline.detection if valid_execution and pipeline is not None else None
156
+ b = (
157
+ detection.b[0].result
158
+ if detection is not None
159
+ else _unavailable(model.reason or "detector_unavailable")
160
+ )
161
+ return EvaluationRecord(
162
+ case_id=case.case_id,
163
+ scenario_id=case.scenario_id,
164
+ group_id=group_id,
165
+ family=scenario.family,
166
+ migration=case.migration,
167
+ engine=cast(Engine, case.pipeline.execution.engine),
168
+ candidate_sha256=digest(case.candidate),
169
+ pipeline_sha256=digest(pipeline) if pipeline is not None else None,
170
+ oracle_sha256=digest(oracle) if oracle is not None else None,
171
+ oracle_state=oracle_state,
172
+ oracle_label=label,
173
+ r=r,
174
+ m=m,
175
+ b=b,
176
+ h=h,
177
+ m_score=cast(float | None, detection.m[0].score) if detection is not None else None,
178
+ b_score=detection.b[0].score if detection is not None else None,
179
+ detector_bundle_sha256=ef.detector_fingerprint(model.bundle) if model.bundle else None,
180
+ freeze_sha256=config.freeze_sha256,
181
+ failure_reason=failure,
182
+ )
183
+
184
+
185
+ def _validate_replay(
186
+ case: TestCaseDefinition,
187
+ scenario: Scenario,
188
+ pipeline: ef.PipelineResult | None,
189
+ replay: ef.ComparisonResult | None,
190
+ ) -> None:
191
+ if replay is None:
192
+ return
193
+ execution = pipeline.execution if pipeline is not None else None
194
+ if execution is None or execution.output is None or execution.state != "completed":
195
+ raise ValueError("output replay requires an actual completed captured output")
196
+ if (
197
+ replay.spec != scenario.comparison
198
+ or replay.reference != "peer"
199
+ or replay.options != case.pipeline.comparison_options
200
+ or (
201
+ replay.left is not None
202
+ and (
203
+ replay.left.table != execution.output
204
+ or not same_content(replay.left.data, execution.output.data)
205
+ )
206
+ )
207
+ or (replay.right is not None and not same_content(replay.right.data, case.candidate.output))
208
+ ):
209
+ raise ValueError("output replay must bind the fresh output and reviewed candidate")
210
+
211
+
212
+ class StudyResult(Document):
213
+ study_id: Text
214
+ freeze: FreezeManifest
215
+ freeze_sha256: Digest
216
+ audit_before: FreezeAudit
217
+ audit_after: FreezeAudit
218
+ started_at: Text
219
+ finished_at: Text
220
+ cases: tuple[StudyCaseResult, ...]
221
+ evaluation: EvaluationReport
222
+ tables: TableExport
223
+ evidence: tuple[ef.EvidenceFile, ...]
224
+
225
+ @model_validator(mode="after")
226
+ def bindings(self) -> Self:
227
+ if self.freeze_sha256 != digest(self.freeze) or self.audit_before != self.freeze.audit:
228
+ raise ValueError("study must start from its exact verified frozen manifest")
229
+ if self.audit_after.definition_sha256 != self.freeze.definition_sha256:
230
+ raise ValueError("after-run audit must inspect the same frozen definition")
231
+ definitions = self.freeze.definition.cases
232
+ if tuple(c.case_id for c in self.cases) != tuple(c.case_id for c in definitions):
233
+ raise ValueError("study must retain each scheduled case exactly once")
234
+ config = _config(self.freeze, self.audit_after)
235
+ scenarios = {s.scenario_id: s for s in self.freeze.definition.catalog.scenarios}
236
+ groups = {a.scenario_id: a.group_id for a in self.freeze.definition.splits.assignments}
237
+ models = {m.family: m for m in self.freeze.definition.models}
238
+ records = []
239
+ for definition, result in zip(definitions, self.cases, strict=True):
240
+ scenario = scenarios[definition.scenario_id]
241
+ _validate_replay(definition, scenario, result.pipeline, result.output_replay)
242
+ expected = _record(
243
+ definition,
244
+ scenario,
245
+ groups[scenario.scenario_id],
246
+ models[scenario.family],
247
+ result.pipeline,
248
+ result.output_replay,
249
+ result.error,
250
+ config,
251
+ )
252
+ if result.record != expected:
253
+ raise ValueError(
254
+ "evaluation record differs from its original frozen case and pipeline"
255
+ )
256
+ records.append(expected)
257
+ if self.evaluation != evaluate_records(tuple(records), config):
258
+ raise ValueError("evaluation must regenerate from every original case result")
259
+ if (
260
+ self.tables.evaluation_sha256 != digest(self.evaluation)
261
+ or self.tables.scientific != self.evaluation.scientific
262
+ ):
263
+ raise ValueError("table export must bind the exact derived evaluation")
264
+ if len({e.path for e in self.evidence}) != len(self.evidence):
265
+ raise ValueError("study evidence paths must be unique")
266
+ return self
267
+
268
+ @property
269
+ def complete(self) -> bool:
270
+ scenarios = {s.scenario_id: s for s in self.freeze.definition.catalog.scenarios}
271
+ return bool(self.cases) and (
272
+ self.audit_after == self.audit_before
273
+ and self.audit_after.ready
274
+ and all(
275
+ c.pipeline is not None
276
+ and c.pipeline.complete
277
+ and _binding_reason(d, scenarios[d.scenario_id], c.pipeline, c.output_replay)
278
+ is None
279
+ for d, c in zip(self.freeze.definition.cases, self.cases, strict=True)
280
+ )
281
+ )
282
+
283
+ @property
284
+ def scientific(self) -> bool:
285
+ return self.complete and self.evaluation.scientific
286
+
287
+ @property
288
+ def exit_code(self) -> int:
289
+ if not self.complete:
290
+ return 3
291
+ return int(any(c.pipeline is not None and c.pipeline.exit_code == 1 for c in self.cases))
292
+
293
+
294
+ def run_study(
295
+ manifest: FreezeManifest,
296
+ *,
297
+ registry: ef.TransformationRegistry,
298
+ rule_registry: ef.RuleRegistry | None = None,
299
+ work_dir: Path | None = None,
300
+ cancellation: ef.CancellationToken | None = None,
301
+ ) -> StudyResult:
302
+ """Consume an existing seal; run each native case once, never train or review.
303
+
304
+ Pilot execution preserves every declared scientific blocker. A failed after
305
+ audit keeps raw evidence but cannot produce a scientific final report.
306
+ Cancellation is cooperative and keeps all remaining scheduled case records.
307
+ """
308
+ manifest = FreezeManifest.model_validate(manifest)
309
+ if any(
310
+ c.pipeline.execution.engine not in ("pandas", "polars", "duckdb")
311
+ for c in manifest.definition.cases
312
+ ):
313
+ raise ValueError("study cases require one of the three declared native engines")
314
+ before = audit_freeze(manifest.definition, rule_registry=rule_registry)
315
+ if before != manifest.audit or not before.ready:
316
+ raise ValueError("frozen sources or gate changed before the study")
317
+ identifier = "study_" + uuid4().hex
318
+ directory = (work_dir or Path.cwd() / "equiframe-studies").resolve() / identifier
319
+ directory.mkdir(parents=True, exist_ok=False)
320
+ save_document(manifest, directory / "freeze.json")
321
+ save_document(before, directory / "audit-before.json")
322
+ started = datetime.now(UTC).isoformat()
323
+ token = cancellation if cancellation is not None else ef.CancellationToken()
324
+ scenarios = {s.scenario_id: s for s in manifest.definition.catalog.scenarios}
325
+ models = {m.family: m for m in manifest.definition.models}
326
+ groups = {a.scenario_id: a.group_id for a in manifest.definition.splits.assignments}
327
+ produced: list[tuple[ef.PipelineResult | None, ef.ComparisonResult | None, str | None]] = []
328
+ for position, case in enumerate(manifest.definition.cases):
329
+ scenario = scenarios[case.scenario_id]
330
+ model = models[scenario.family]
331
+ folder = directory / f"case-{position}"
332
+ folder.mkdir()
333
+ pipeline = None
334
+ replay = None
335
+ error = None
336
+ try:
337
+ pipeline = ef.run_pipeline(
338
+ case.pipeline,
339
+ source=ef.LocalDataSource({i.name: i.artifact for i in scenario.inputs}),
340
+ registry=registry,
341
+ rule_registry=rule_registry,
342
+ detector_bundle=model.bundle,
343
+ comparison=scenario.expected,
344
+ cancellation=token,
345
+ work_dir=folder,
346
+ )
347
+ except Exception as exc:
348
+ error = "pipeline_operation_failed:" + type(exc).__name__
349
+ execution = pipeline.execution if pipeline is not None else None
350
+ if (
351
+ execution is not None
352
+ and execution.state == "completed"
353
+ and execution.output is not None
354
+ ):
355
+ try:
356
+ replay = ef.compare(
357
+ execution.output,
358
+ case.candidate.output,
359
+ contract=scenario.comparison.contract,
360
+ policy=scenario.comparison.policy,
361
+ options=case.pipeline.comparison_options,
362
+ reference="peer",
363
+ cancellation=token,
364
+ work_dir=folder / "replay",
365
+ )
366
+ except Exception:
367
+ # Its partial files remain inventoried. No replay means no label reuse.
368
+ replay = None
369
+ produced.append((pipeline, replay, error))
370
+ after = audit_freeze(manifest.definition, rule_registry=rule_registry)
371
+ save_document(after, directory / "audit-after.json")
372
+ config = _config(manifest, after)
373
+ results = []
374
+ for case, (pipeline, replay, error) in zip(manifest.definition.cases, produced, strict=True):
375
+ scenario = scenarios[case.scenario_id]
376
+ record = _record(
377
+ case,
378
+ scenario,
379
+ groups[scenario.scenario_id],
380
+ models[scenario.family],
381
+ pipeline,
382
+ replay,
383
+ error,
384
+ config,
385
+ )
386
+ case_result = StudyCaseResult(
387
+ case_id=case.case_id,
388
+ pipeline_directory=pipeline.pipeline_id if pipeline is not None else None,
389
+ pipeline=pipeline,
390
+ output_replay=replay,
391
+ error=error,
392
+ record=record,
393
+ )
394
+ results.append(case_result)
395
+ evaluation = evaluate_records(tuple(r.record for r in results), config)
396
+ save_document(evaluation, directory / "evaluation.json")
397
+ tables = write_tables(evaluation, directory / "tables")
398
+ for position, result in enumerate(results):
399
+ save_document(result, directory / f"case-{position}" / "case.json")
400
+ study_result = StudyResult(
401
+ study_id=identifier,
402
+ freeze=manifest,
403
+ freeze_sha256=digest(manifest),
404
+ audit_before=before,
405
+ audit_after=after,
406
+ started_at=started,
407
+ finished_at=datetime.now(UTC).isoformat(),
408
+ cases=tuple(results),
409
+ evaluation=evaluation,
410
+ tables=tables,
411
+ evidence=inventory(directory),
412
+ )
413
+ save_document(study_result, directory / "study.json")
414
+ return study_result
415
+
416
+
417
+ def _owned(directory: Path, reference: ef.ArtifactRef, listed: dict[str, ef.EvidenceFile]) -> None:
418
+ path = local_path(reference).resolve()
419
+ if not path.is_relative_to(directory):
420
+ raise ef.ArtifactIntegrityError("study comparison evidence escapes its operation")
421
+ item = listed.get(path.relative_to(directory).as_posix())
422
+ if item is None or (item.sha256, item.size_bytes) != (reference.sha256, reference.size_bytes):
423
+ raise ef.ArtifactIntegrityError("study comparison artifact contradicts its inventory")
424
+
425
+
426
+ def _recheck_confirmed_replay(
427
+ definition: TestCaseDefinition,
428
+ scenario: Scenario,
429
+ case: StudyCaseResult,
430
+ ) -> None:
431
+ """Recheck captured rows that authorize reuse of a confirmed review label."""
432
+ if case.record.oracle_state != "confirmed":
433
+ return
434
+ execution = case.pipeline.execution if case.pipeline is not None else None
435
+ replay = case.output_replay
436
+ if (
437
+ execution is None
438
+ or execution.state != "completed"
439
+ or execution.output is None
440
+ or replay is None
441
+ or not replay.complete
442
+ or replay.state != "equivalent"
443
+ ):
444
+ raise ef.ArtifactIntegrityError("confirmed label requires a complete output replay")
445
+ try:
446
+ with TemporaryDirectory(prefix="equiframe-study-recheck-") as temporary:
447
+ observed = ef.compare(
448
+ execution.output,
449
+ definition.candidate.output,
450
+ contract=scenario.comparison.contract,
451
+ policy=scenario.comparison.policy,
452
+ options=definition.pipeline.comparison_options,
453
+ reference="peer",
454
+ work_dir=Path(temporary),
455
+ )
456
+ except Exception as exc:
457
+ raise ef.ArtifactIntegrityError("confirmed replay physical verification failed") from exc
458
+ if not observed.complete or observed.state != "equivalent" or observed.counts != replay.counts:
459
+ raise ef.ArtifactIntegrityError(
460
+ "confirmed label requires the physically reproduced reviewed output"
461
+ )
462
+
463
+
464
+ def read_study(
465
+ directory: Path,
466
+ *,
467
+ rule_registry: ef.RuleRegistry | None = None,
468
+ max_bytes: int = 64 * 1024**2,
469
+ ) -> StudyResult:
470
+ """Verify raw cases, pipeline evidence and regenerated tables without native ETL."""
471
+ path = directory / "study.json" if directory.is_dir() else directory
472
+ if type(max_bytes) is not int or max_bytes < 1:
473
+ raise ValueError("study JSON byte budget must be a positive integer")
474
+ result, original_sha256 = _bounded_json(StudyResult, path, max_bytes)
475
+ directory = path.parent.resolve()
476
+ verify_files(directory, result.evidence)
477
+ listed = {item.path: item for item in result.evidence}
478
+ actual = {
479
+ p.relative_to(directory).as_posix()
480
+ for p in directory.rglob("*")
481
+ if p.is_file() and p != path.resolve()
482
+ }
483
+ if actual != set(listed):
484
+ raise ef.ArtifactIntegrityError("study operation inventory is not closed")
485
+ for name, model, expected in (
486
+ ("freeze.json", FreezeManifest, result.freeze),
487
+ ("audit-before.json", FreezeAudit, result.audit_before),
488
+ ("audit-after.json", FreezeAudit, result.audit_after),
489
+ ("evaluation.json", EvaluationReport, result.evaluation),
490
+ ):
491
+ if name not in listed or load_document(model, directory / name) != expected:
492
+ raise ef.ArtifactIntegrityError("study source manifest contradicts final evidence")
493
+ scenarios = {s.scenario_id: s for s in result.freeze.definition.catalog.scenarios}
494
+ for position, case in enumerate(result.cases):
495
+ folder = directory / f"case-{position}"
496
+ if load_document(StudyCaseResult, folder / "case.json") != case:
497
+ raise ef.ArtifactIntegrityError("case receipt contradicts the final study")
498
+ if (
499
+ case.pipeline is not None
500
+ and ef.read_pipeline(folder / case.pipeline.pipeline_id) != case.pipeline
501
+ ):
502
+ raise ef.ArtifactIntegrityError("pipeline evidence contradicts its study case")
503
+ if case.output_replay is not None:
504
+ replay = case.output_replay
505
+ for ref in (replay.differences, *(s.data for s in (replay.left, replay.right) if s)):
506
+ _owned(directory, ref, listed)
507
+ manifest_path = local_path(replay.differences).parent / "comparison.json"
508
+ if ef.read_comparison(manifest_path) != replay:
509
+ raise ef.ArtifactIntegrityError("output replay differs from its original manifest")
510
+ if (
511
+ len(tuple(ef.iter_differences(replay.differences)))
512
+ != replay.counts.difference_records
513
+ ):
514
+ raise ef.ArtifactIntegrityError(
515
+ "output replay count differs from its actual evidence"
516
+ )
517
+ definition = result.freeze.definition.cases[position]
518
+ _recheck_confirmed_replay(definition, scenarios[definition.scenario_id], case)
519
+ if read_tables(directory / "tables", report=result.evaluation) != result.tables:
520
+ raise ef.ArtifactIntegrityError("tables contradict the study evaluation")
521
+ observed = audit_freeze(result.freeze.definition, rule_registry=rule_registry)
522
+ if observed != result.audit_after:
523
+ raise ef.ArtifactIntegrityError("frozen sources no longer match the after-study audit")
524
+ if _stream_file(path, max_bytes)[0] != original_sha256:
525
+ raise ef.ArtifactIntegrityError("study manifest changed during verification")
526
+ return result