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.
- equiframe_benchmark/PROTOCOL_V2.md +172 -0
- equiframe_benchmark/__init__.py +284 -0
- equiframe_benchmark/__main__.py +3 -0
- equiframe_benchmark/_scale_workload.py +375 -0
- equiframe_benchmark/calibration_types.py +112 -0
- equiframe_benchmark/cli.py +196 -0
- equiframe_benchmark/commands.py +149 -0
- equiframe_benchmark/commands_scale.py +38 -0
- equiframe_benchmark/commands_step10.py +275 -0
- equiframe_benchmark/commands_v2.py +206 -0
- equiframe_benchmark/cost_table.py +62 -0
- equiframe_benchmark/costs.py +788 -0
- equiframe_benchmark/distribution.py +932 -0
- equiframe_benchmark/evaluation.py +614 -0
- equiframe_benchmark/evaluation_v2.py +428 -0
- equiframe_benchmark/fixtures.py +299 -0
- equiframe_benchmark/freeze.py +869 -0
- equiframe_benchmark/freeze_v2.py +412 -0
- equiframe_benchmark/integration.py +655 -0
- equiframe_benchmark/io.py +29 -0
- equiframe_benchmark/models.py +313 -0
- equiframe_benchmark/models_v2.py +13 -0
- equiframe_benchmark/mutations.py +34 -0
- equiframe_benchmark/oracle.py +264 -0
- equiframe_benchmark/pairwise.py +248 -0
- equiframe_benchmark/pilot.py +254 -0
- equiframe_benchmark/py.typed +0 -0
- equiframe_benchmark/reconciliation.py +294 -0
- equiframe_benchmark/reproduction.py +542 -0
- equiframe_benchmark/scale_io.py +19 -0
- equiframe_benchmark/scale_process.py +1077 -0
- equiframe_benchmark/scale_types.py +120 -0
- equiframe_benchmark/scaling.py +529 -0
- equiframe_benchmark/schemas/protocol-v2/CommonSupportV2-2.0.schema.json +327 -0
- equiframe_benchmark/schemas/protocol-v2/ContrastSpecV2-2.0.schema.json +37 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationConfigV2-2.0.schema.json +184 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationReportV2-2.0.schema.json +1237 -0
- equiframe_benchmark/schemas/protocol-v2/EvaluationStratumV2-2.0.schema.json +747 -0
- equiframe_benchmark/schemas/protocol-v2/FreezeDefinitionV2-2.0.schema.json +5951 -0
- equiframe_benchmark/schemas/protocol-v2/FreezeManifestV2-2.0.schema.json +6222 -0
- equiframe_benchmark/schemas/protocol-v2/PrincipalContrastV2-2.0.schema.json +578 -0
- equiframe_benchmark/schemas/protocol-v2/ProtocolReviewV2-2.0.schema.json +56 -0
- equiframe_benchmark/schemas/protocol-v2/SignedMetricV2-2.0.schema.json +51 -0
- equiframe_benchmark/schemas/protocol-v2/StudyCaseResultV2-2.0.schema.json +4881 -0
- equiframe_benchmark/schemas/protocol-v2/StudyResultV2-2.0.schema.json +8370 -0
- equiframe_benchmark/schemas/protocol-v2/TableExportV2-2.0.schema.json +75 -0
- equiframe_benchmark/selection.py +178 -0
- equiframe_benchmark/snapshot.py +24 -0
- equiframe_benchmark/splits.py +92 -0
- equiframe_benchmark/study.py +526 -0
- equiframe_benchmark/study_bundle.py +379 -0
- equiframe_benchmark/study_bundle_v2.py +388 -0
- equiframe_benchmark/study_v2.py +544 -0
- equiframe_benchmark/tables.py +347 -0
- equiframe_benchmark/tables_v2.py +340 -0
- equiframe_benchmark/training_types.py +52 -0
- equiframe_benchmark-0.12.4.dist-info/METADATA +343 -0
- equiframe_benchmark-0.12.4.dist-info/RECORD +62 -0
- equiframe_benchmark-0.12.4.dist-info/WHEEL +4 -0
- equiframe_benchmark-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe_benchmark-0.12.4.dist-info/licenses/LICENSE +21 -0
- 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
|