equiframe 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 (170) hide show
  1. equiframe/__init__.py +832 -0
  2. equiframe/__main__.py +3 -0
  3. equiframe/_base.py +84 -0
  4. equiframe/_cli_support.py +125 -0
  5. equiframe/_comparison_io.py +161 -0
  6. equiframe/_dataset_provenance.py +146 -0
  7. equiframe/_decimal.py +22 -0
  8. equiframe/_diff_capture.py +433 -0
  9. equiframe/_doctor.py +123 -0
  10. equiframe/_document_io.py +61 -0
  11. equiframe/_documents.py +79 -0
  12. equiframe/_duckdb_transport.py +149 -0
  13. equiframe/_error_reporting.py +52 -0
  14. equiframe/_first_use.py +102 -0
  15. equiframe/_path_compat.py +26 -0
  16. equiframe/_physical.py +133 -0
  17. equiframe/_r_review.R +308 -0
  18. equiframe/_regular_io.py +167 -0
  19. equiframe/_report_format_v1.py +298 -0
  20. equiframe/_report_io.py +591 -0
  21. equiframe/_stability_inventory.py +1421 -0
  22. equiframe/_timezones.py +257 -0
  23. equiframe/_vendor/cyclonedx/LICENSE +201 -0
  24. equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
  25. equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
  26. equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
  27. equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
  28. equiframe/adapters.py +454 -0
  29. equiframe/assertion_types.py +122 -0
  30. equiframe/assertions.py +271 -0
  31. equiframe/benchmark.py +101 -0
  32. equiframe/bundle.py +240 -0
  33. equiframe/cli.py +693 -0
  34. equiframe/comparison.py +937 -0
  35. equiframe/comparison_report.py +660 -0
  36. equiframe/comparison_types.py +257 -0
  37. equiframe/compatibility.py +546 -0
  38. equiframe/contracts.py +879 -0
  39. equiframe/datasets.py +1301 -0
  40. equiframe/decision.py +34 -0
  41. equiframe/destinations.py +295 -0
  42. equiframe/detector_types.py +431 -0
  43. equiframe/detectors.py +389 -0
  44. equiframe/diff.py +192 -0
  45. equiframe/diff_contracts.py +523 -0
  46. equiframe/docs/adoption.md +329 -0
  47. equiframe/docs/api.md +534 -0
  48. equiframe/docs/arquitecto.md +143 -0
  49. equiframe/docs/choosing-api.md +84 -0
  50. equiframe/docs/cientifico.md +180 -0
  51. equiframe/docs/compatibility.md +254 -0
  52. equiframe/docs/contributing.md +171 -0
  53. equiframe/docs/dependencies.md +237 -0
  54. equiframe/docs/eda.md +166 -0
  55. equiframe/docs/extending.md +158 -0
  56. equiframe/docs/fast-ci.md +52 -0
  57. equiframe/docs/first-use.md +78 -0
  58. equiframe/docs/index.md +142 -0
  59. equiframe/docs/ingestion.md +145 -0
  60. equiframe/docs/inicio.md +129 -0
  61. equiframe/docs/installed-private.md +97 -0
  62. equiframe/docs/journeys.md +362 -0
  63. equiframe/docs/lectura-conversion.md +211 -0
  64. equiframe/docs/licensing.md +17 -0
  65. equiframe/docs/maintenance.md +233 -0
  66. equiframe/docs/migration-ir.md +333 -0
  67. equiframe/docs/option-audit.md +45 -0
  68. equiframe/docs/outcome-gates.md +134 -0
  69. equiframe/docs/performance.md +263 -0
  70. equiframe/docs/piloto-simulado.md +56 -0
  71. equiframe/docs/quickstart.md +156 -0
  72. equiframe/docs/r_review.md +212 -0
  73. equiframe/docs/recipes.md +248 -0
  74. equiframe/docs/releasing.md +183 -0
  75. equiframe/docs/security.md +218 -0
  76. equiframe/docs/semantics.md +274 -0
  77. equiframe/docs/sqlite-snapshots.md +262 -0
  78. equiframe/docs/universal-sources.md +49 -0
  79. equiframe/docs/universalidad-cinco-pasos.md +107 -0
  80. equiframe/docs/upgrading.md +538 -0
  81. equiframe/duckdb_capture.py +410 -0
  82. equiframe/eda/__init__.py +36 -0
  83. equiframe/eda/__main__.py +3 -0
  84. equiframe/eda/_backend.py +612 -0
  85. equiframe/eda/_inventory.py +49 -0
  86. equiframe/eda/_paths.py +49 -0
  87. equiframe/eda/_report.py +291 -0
  88. equiframe/eda/api.py +535 -0
  89. equiframe/eda/cli.py +102 -0
  90. equiframe/eda/models.py +239 -0
  91. equiframe/errors.py +83 -0
  92. equiframe/evidence.py +375 -0
  93. equiframe/examples/integrations/eda_generic.py +60 -0
  94. equiframe/examples/integrations/outcome_gates.py +94 -0
  95. equiframe/examples/integrations/sqlite_snapshot.py +246 -0
  96. equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
  97. equiframe/examples/integrations/universal_first_use.py +296 -0
  98. equiframe/examples/journeys/README.md +31 -0
  99. equiframe/examples/journeys/__init__.py +1 -0
  100. equiframe/examples/journeys/a_pandas_refactor.py +88 -0
  101. equiframe/examples/journeys/b_native_migration.py +181 -0
  102. equiframe/examples/journeys/c_retail_etl.py +460 -0
  103. equiframe/examples/journeys/common.py +136 -0
  104. equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
  105. equiframe/examples/journeys/e_telemetry.py +135 -0
  106. equiframe/examples/journeys/f_data_science_preparation.py +84 -0
  107. equiframe/examples/journeys/g_external_snapshots.py +197 -0
  108. equiframe/examples/journeys/h_portable_ci.py +129 -0
  109. equiframe/examples/journeys/requirements.lock +249 -0
  110. equiframe/examples/journeys/verify_journeys.py +117 -0
  111. equiframe/examples/load_convert_compare.py +85 -0
  112. equiframe/execution.py +355 -0
  113. equiframe/execution_types.py +177 -0
  114. equiframe/ingestion.py +744 -0
  115. equiframe/local.py +126 -0
  116. equiframe/migration/__init__.py +94 -0
  117. equiframe/migration/_physical.py +8 -0
  118. equiframe/migration/backends/_timestamp_transport.py +109 -0
  119. equiframe/migration/backends/duckdb.py +651 -0
  120. equiframe/migration/backends/duckdb_decimal.py +125 -0
  121. equiframe/migration/backends/duckdb_time.py +87 -0
  122. equiframe/migration/backends/pandas.py +544 -0
  123. equiframe/migration/backends/polars.py +915 -0
  124. equiframe/migration/backends/polars_decimal.py +231 -0
  125. equiframe/migration/io.py +198 -0
  126. equiframe/migration/ports.py +96 -0
  127. equiframe/migration/runner.py +545 -0
  128. equiframe/migration/runner_types.py +405 -0
  129. equiframe/migration/types.py +354 -0
  130. equiframe/migration/validation.py +619 -0
  131. equiframe/normalization.py +358 -0
  132. equiframe/pipeline.py +274 -0
  133. equiframe/pipeline_types.py +247 -0
  134. equiframe/ports.py +39 -0
  135. equiframe/preprocessing.py +110 -0
  136. equiframe/profile_types.py +343 -0
  137. equiframe/profiles.py +447 -0
  138. equiframe/py.typed +0 -0
  139. equiframe/r_review.py +706 -0
  140. equiframe/reduction.py +518 -0
  141. equiframe/registry.py +121 -0
  142. equiframe/report_rendering.py +366 -0
  143. equiframe/report_types.py +294 -0
  144. equiframe/reporting.py +26 -0
  145. equiframe/rule_types.py +178 -0
  146. equiframe/rules.py +819 -0
  147. equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
  148. equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
  149. equiframe/schemas/r-review-options-1.0.schema.json +38 -0
  150. equiframe/schemas/r-review-result-1.0.schema.json +207 -0
  151. equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
  152. equiframe/serialization.py +152 -0
  153. equiframe/snapshot.py +296 -0
  154. equiframe/sources.py +283 -0
  155. equiframe/tables.py +459 -0
  156. equiframe/types.py +71 -0
  157. equiframe/values.py +252 -0
  158. equiframe/verification_request.py +35 -0
  159. equiframe/verify.py +799 -0
  160. equiframe/verify_types.py +255 -0
  161. equiframe/workflow.py +510 -0
  162. equiframe/workflow_request.py +38 -0
  163. equiframe-0.12.4.dist-info/METADATA +268 -0
  164. equiframe-0.12.4.dist-info/RECORD +170 -0
  165. equiframe-0.12.4.dist-info/WHEEL +4 -0
  166. equiframe-0.12.4.dist-info/entry_points.txt +3 -0
  167. equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
  168. equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
  169. equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
  170. equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
equiframe/verify.py ADDED
@@ -0,0 +1,799 @@
1
+ """Inspect existing results without rerunning their ETL or requiring the benchmark."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Mapping
7
+ from datetime import datetime
8
+ from datetime import timezone as _timezone
9
+ from itertools import combinations
10
+ from pathlib import Path
11
+ from uuid import uuid4
12
+
13
+ from typing_extensions import TypeAliasType
14
+
15
+ from equiframe._comparison_io import snapshot
16
+ from equiframe._error_reporting import report_failure
17
+ from equiframe._physical import verify_table_metadata
18
+ from equiframe._regular_io import local_output_directory
19
+ from equiframe._report_format_v1 import verification_html_v1, verification_summary_v1
20
+ from equiframe.comparison import compare, iter_differences, read_comparison
21
+ from equiframe.comparison_types import ComparisonOptions, ComparisonPolicy, ComparisonResult
22
+ from equiframe.contracts import Contract, TableSpec
23
+ from equiframe.errors import ArtifactIntegrityError
24
+ from equiframe.evidence import inventory, load_json, parse_json, publish_json, verify_files
25
+ from equiframe.execution_types import ExecutionResult, TableArtifact
26
+ from equiframe.local import local_path
27
+ from equiframe.pipeline_types import PipelineResult
28
+ from equiframe.rule_types import RuleEvaluation
29
+ from equiframe.rules import evaluate_rules, iter_rule_findings, read_rule_evaluation
30
+ from equiframe.serialization import contract_fingerprint
31
+ from equiframe.tables import read_table
32
+ from equiframe.types import ArtifactRef
33
+ from equiframe.verify_types import (
34
+ ExpectedProvenance,
35
+ IndependentReviewEvidence,
36
+ VerificationBinding,
37
+ VerificationCheck,
38
+ VerificationCoverage,
39
+ VerificationResult,
40
+ )
41
+
42
+ UTC = _timezone.utc
43
+
44
+ VerificationInput = TypeAliasType(
45
+ "VerificationInput", ArtifactRef | ExecutionResult | PipelineResult
46
+ )
47
+
48
+
49
+ def _check(
50
+ state: str, reason: str, output: str | None = None, comparison: ComparisonResult | None = None
51
+ ) -> VerificationCheck:
52
+ return VerificationCheck.model_validate(
53
+ dict(
54
+ state=state,
55
+ reason=reason,
56
+ output=output,
57
+ comparison_id=comparison.comparison_id if comparison is not None else None,
58
+ )
59
+ )
60
+
61
+
62
+ def _comparison_check(c: ComparisonResult, output: str | None = None) -> VerificationCheck:
63
+ state = {"equivalent": "passed", "different": "failed", "not_evaluable": "not_evaluable"}
64
+ return _check(state[c.state], f"Complete output comparison: {c.state}", output, c)
65
+
66
+
67
+ def _aggregate(checks: tuple[VerificationCheck, ...], reason: str) -> VerificationCheck:
68
+ state = "passed"
69
+ if any(c.state == "failed" for c in checks):
70
+ state = "failed"
71
+ elif any(c.state == "not_evaluable" for c in checks):
72
+ state = "not_evaluable"
73
+ elif any(c.state == "pending_review" for c in checks):
74
+ state = "pending_review"
75
+ return _check(state, reason)
76
+
77
+
78
+ def _review(
79
+ binding: VerificationBinding,
80
+ provenance: ExpectedProvenance | None,
81
+ reviews: tuple[IndependentReviewEvidence, ...],
82
+ expected_checks: tuple[VerificationCheck, ...],
83
+ requested: bool,
84
+ ) -> VerificationCheck:
85
+ if not requested:
86
+ return _check("not_requested", "Scientific O was not requested")
87
+ if not binding.expected_sha256:
88
+ return _check("not_evaluable", "Scientific O requires an expected artifact")
89
+ if provenance is None or not provenance.independently_constructed:
90
+ return _check("not_evaluable", "Independent expected construction is not established")
91
+ if not binding.snapshots or binding.plan_sha256 is None:
92
+ return _check("not_evaluable", "Scientific O requires snapshot and plan identities")
93
+ by_output = {r.output: r for r in reviews}
94
+ if len(by_output) != len(reviews):
95
+ raise ValueError("duplicate review evidence for an output")
96
+ states: list[VerificationCheck] = []
97
+ prohibited = set(provenance.authors + provenance.implementation_authors)
98
+ for check in expected_checks:
99
+ review = by_output.get(check.output or "")
100
+ if review is None:
101
+ states.append(_check("pending_review", "Two independent blind reviews required"))
102
+ continue
103
+ rb = review.binding
104
+ if (
105
+ rb.contract_sha256 != binding.contract_sha256
106
+ or rb.snapshots != binding.snapshots
107
+ or rb.plan_sha256 != binding.plan_sha256
108
+ or rb.expected_sha256 != binding.expected_sha256
109
+ or dict(rb.outputs).get(review.output) != dict(binding.outputs).get(review.output)
110
+ ):
111
+ raise ValueError("review identity differs from verified content")
112
+ if prohibited.intersection((*review.reviewers, review.arbitrator)):
113
+ states.append(_check("not_evaluable", "Reviewers overlap declared authors"))
114
+ elif check.state == "not_evaluable":
115
+ states.append(_check("not_evaluable", "Expected comparison is incomplete"))
116
+ elif len(review.reviews) != 2:
117
+ states.append(_check("pending_review", "Two primary reviews are required"))
118
+ elif review.reviews[0].label != review.reviews[1].label and review.resolution is None:
119
+ states.append(_check("pending_review", "Independent arbitration is required"))
120
+ else:
121
+ selected = review.resolution.label if review.resolution else review.reviews[0].label
122
+ observed = "normal" if check.state == "passed" else "divergent"
123
+ states.append(
124
+ _check(
125
+ "not_evaluable"
126
+ if selected == "not_evaluable"
127
+ else "passed"
128
+ if selected == observed
129
+ else "failed",
130
+ "Declared independent human decisions checked against the expected comparison",
131
+ )
132
+ )
133
+ return _aggregate(
134
+ tuple(states),
135
+ "Scientific O checks declared provenance and reviews; identities are not authenticated",
136
+ )
137
+
138
+
139
+ def _validate_dimensions(result: VerificationResult) -> None:
140
+ """Reconstruct decisions from bound component records, including pending O."""
141
+ if result.integrity.state not in ("passed", "failed", "not_evaluable"):
142
+ raise ValueError("verification integrity cannot be skipped or pending review")
143
+ by_id = {c.comparison_id: c for c in result.comparisons}
144
+ if len(by_id) != len(result.comparisons):
145
+ raise ValueError("verification comparison identities must be unique")
146
+ refs = dict(result.outputs)
147
+ executions = dict(result.executions)
148
+ if len(executions) != len(result.executions) or not set(executions) <= {
149
+ c.output for c in result.technical_outcomes
150
+ }:
151
+ raise ValueError("captured execution output identities must be unique and requested")
152
+ for name, captured_execution in result.executions:
153
+ if captured_execution.plan.contract != result.contract or (
154
+ captured_execution.output is not None
155
+ and (name not in refs or captured_execution.output.data.sha256 != refs[name].sha256)
156
+ ):
157
+ raise ValueError("captured execution contradicts verified contract or output")
158
+ snapshot_identity = sorted((name, ref.sha256) for name, ref in result.input_artifacts)
159
+ input_identity_mismatch = any(
160
+ sorted((table.name, table.data.sha256) for table in execution.inputs) != snapshot_identity
161
+ for execution in executions.values()
162
+ )
163
+ if input_identity_mismatch and (result.integrity.state != "failed" or result.coverage.complete):
164
+ raise ValueError(
165
+ "execution inputs differ from bound snapshots; failed integrity and "
166
+ "incomplete coverage must remain recorded"
167
+ )
168
+ if result.coverage.complete and result.integrity.state != "passed":
169
+ raise ValueError("complete verification requires passed integrity")
170
+ for component in (*result.comparisons, *result.input_comparisons):
171
+ if (
172
+ component.spec.policy != result.comparison_policy
173
+ or component.options != result.comparison_options
174
+ ):
175
+ raise ValueError("comparison policy or options contradict verification request")
176
+
177
+ def bound(c: ComparisonResult, left: str, right: str | None) -> None:
178
+ if c.spec.contract != result.contract:
179
+ raise ValueError("comparison contract contradicts verification request")
180
+ for source, name in ((c.left, left), (c.right, right)):
181
+ ref = refs[name] if name is not None else result.expected_artifact
182
+ if ref is None or (source is not None and source.data.sha256 != ref.sha256):
183
+ raise ValueError("comparison content identities differ from requested pair")
184
+ if source is not None:
185
+ execution = executions.get(name or "")
186
+ if source.execution != execution:
187
+ raise ValueError("comparison execution contradicts captured execution")
188
+
189
+ names = tuple(c.output for c in result.expected)
190
+ if (
191
+ len(names) != result.coverage.outputs_requested
192
+ or len(set(names)) != len(names)
193
+ or None in names
194
+ or tuple(c.output for c in result.rules) != names
195
+ or tuple(c.output for c in result.technical_outcomes) != names
196
+ or not set(refs) <= set(names)
197
+ ):
198
+ raise ValueError("verification dimensions must cover each requested output once")
199
+ for check in result.expected:
200
+ if check.comparison_id:
201
+ c = by_id.get(check.comparison_id)
202
+ if (
203
+ c is None
204
+ or c.reference != "expected"
205
+ or check != _comparison_check(c, check.output)
206
+ ):
207
+ raise ValueError("expected dimension contradicts its comparison")
208
+ ref = refs.get(check.output or "")
209
+ if (
210
+ ref is None
211
+ or result.expected_artifact is None
212
+ or (c.left is not None and c.left.data.sha256 != ref.sha256)
213
+ or (c.right is not None and c.right.data.sha256 != result.expected_artifact.sha256)
214
+ ):
215
+ raise ValueError("expected comparison content identities differ")
216
+ bound(c, check.output or "", None)
217
+ else:
218
+ if check.state not in ("not_requested", "not_evaluable"):
219
+ raise ValueError("expected outcome requires a bound comparison")
220
+ if (
221
+ result.expected_artifact is not None or result.expected_required
222
+ ) and check.state != "not_evaluable":
223
+ raise ValueError("requested expected comparison cannot be skipped")
224
+ inspected = len(result.outputs)
225
+ self_comparisons = result.comparisons[:inspected]
226
+ if len(self_comparisons) != inspected:
227
+ raise ValueError("each inspected output requires its complete schema comparison")
228
+ for (name, _), c in zip(result.outputs, self_comparisons, strict=True):
229
+ if c.reference != "peer":
230
+ raise ValueError("output self-comparison reference differs")
231
+ bound(c, name, name)
232
+ if len(result.input_comparisons) != len(result.input_artifacts):
233
+ raise ValueError("each bound input requires its constraint comparison")
234
+ for (name, ref), c in zip(result.input_artifacts, result.input_comparisons, strict=True):
235
+ declared = next((t for t in result.contract.inputs if t.name == name), None)
236
+ if (
237
+ declared is None
238
+ or c.spec.contract != _input_contract(result.contract, declared)
239
+ or c.reference != "peer"
240
+ or any(s is not None and s.data.sha256 != ref.sha256 for s in (c.left, c.right))
241
+ ):
242
+ raise ValueError("input constraint comparison identity differs")
243
+ input_checks = tuple(_comparison_check(c) for c in result.input_comparisons)
244
+ output_checks = tuple(
245
+ _comparison_check(c, name)
246
+ for (name, _), c in zip(result.outputs, self_comparisons, strict=True)
247
+ )
248
+ expected_inputs = (
249
+ _aggregate(
250
+ (*input_checks, *output_checks), "Complete output schema, keys and value constraints"
251
+ )
252
+ if inspected == result.coverage.outputs_requested
253
+ else _check("not_evaluable", "Output capture incomplete")
254
+ )
255
+ if result.inputs != expected_inputs:
256
+ raise ValueError("input dimension contradicts complete constraint comparisons")
257
+ expected_ids = {c.comparison_id for c in result.expected if c.comparison_id}
258
+ peer = tuple(c for c in result.comparisons[inspected:] if c.comparison_id not in expected_ids)
259
+ peer_requested = result.coverage.outputs_requested > 1
260
+ if (
261
+ inspected == result.coverage.outputs_requested
262
+ and len(peer) != inspected * (inspected - 1) // 2
263
+ ):
264
+ raise ValueError("verification must inspect every requested pair independently")
265
+ if len(peer) != inspected * (inspected - 1) // 2:
266
+ raise ValueError("captured outputs require each inspected pair exactly once")
267
+ for ((left, _), (right, _)), c in zip(combinations(result.outputs, 2), peer, strict=True):
268
+ if c.reference != "peer":
269
+ raise ValueError("peer comparison reference differs")
270
+ bound(c, left, right)
271
+ expected_agreement = (
272
+ _aggregate(
273
+ tuple(_comparison_check(c) for c in peer), "All requested pairs compared independently"
274
+ )
275
+ if peer_requested and inspected == result.coverage.outputs_requested
276
+ else _check("not_evaluable" if peer_requested else "not_requested", "Peer comparison scope")
277
+ )
278
+ if result.agreement != expected_agreement:
279
+ raise ValueError("agreement dimension contradicts peer comparisons")
280
+ expected_technical = _aggregate(
281
+ result.technical_outcomes, "Technical outcomes retained separately"
282
+ )
283
+ if not any(c.state != "not_requested" for c in result.technical_outcomes):
284
+ expected_technical = _check(
285
+ "not_requested", "Artifact inspection does not assert ETL execution"
286
+ )
287
+ if result.technical != expected_technical:
288
+ raise ValueError("technical dimension contradicts captured outcomes")
289
+ for check in result.technical_outcomes:
290
+ execution = executions.get(check.output or "")
291
+ if execution is not None:
292
+ observed = _check(
293
+ "passed" if execution.state == "completed" else "not_evaluable",
294
+ "Captured execution state: " + execution.state,
295
+ check.output,
296
+ )
297
+ if check != observed:
298
+ raise ValueError("technical outcome contradicts captured execution state")
299
+ elif check.state not in ("not_requested", "not_evaluable"):
300
+ raise ValueError("technical success requires captured execution evidence")
301
+ evaluations = {r.execution_id: r for r in result.rule_evaluations}
302
+ for check in result.rules:
303
+ execution = executions.get(check.output or "")
304
+ evaluation = evaluations.get(execution.execution_id) if execution is not None else None
305
+ if evaluation is not None:
306
+ state = (
307
+ "failed"
308
+ if evaluation.decision == "alert"
309
+ else "passed"
310
+ if evaluation.complete
311
+ else "not_evaluable"
312
+ )
313
+ expected_rule = _check(state, "Selected rules: " + evaluation.decision, check.output)
314
+ if evaluation.contract_sha256 != result.binding.contract_sha256:
315
+ raise ValueError("selected rules belong to another contract")
316
+ else:
317
+ expected_rule = _check(
318
+ "not_evaluable" if result.rules_required else "not_requested",
319
+ "Rules require captured execution inputs"
320
+ if result.rules_required
321
+ else "No operational rules requested for this artifact",
322
+ check.output,
323
+ )
324
+ if execution is not None and result.contract.rules:
325
+ raise ValueError("captured execution requires its selected rule evaluations")
326
+ if check != expected_rule:
327
+ raise ValueError("rule dimension contradicts selected rule evaluations")
328
+ expected_review = _review(
329
+ result.binding,
330
+ result.provenance,
331
+ result.review_evidence,
332
+ result.expected,
333
+ result.review_requested,
334
+ )
335
+ if result.independent_review != expected_review:
336
+ raise ValueError("independent-review dimension contradicts declared evidence")
337
+ completed = sum(c.complete for c in (*result.comparisons, *result.input_comparisons))
338
+ requested_outputs = result.coverage.outputs_requested
339
+ expected_requested = (
340
+ result.expected_artifact is not None
341
+ or result.expected_required
342
+ or any(check.state != "not_requested" for check in result.expected)
343
+ )
344
+ requested_comparisons = (
345
+ requested_outputs
346
+ + requested_outputs * (requested_outputs - 1) // 2
347
+ + len(result.input_artifacts)
348
+ + (requested_outputs if expected_requested else 0)
349
+ )
350
+ if result.coverage.comparisons_requested != requested_comparisons:
351
+ raise ValueError("coverage contradicts requested comparison dimensions")
352
+ if (
353
+ result.coverage.comparisons_completed != completed
354
+ or result.coverage.outputs_inspected != inspected
355
+ ):
356
+ raise ValueError("coverage contradicts inspected component records")
357
+ complete = (
358
+ inspected == requested_outputs
359
+ and result.integrity.state == "passed"
360
+ and len(result.comparisons) + len(result.input_comparisons) == requested_comparisons
361
+ and all(c.complete for c in (*result.comparisons, *result.input_comparisons))
362
+ )
363
+ if result.coverage.complete != complete:
364
+ raise ValueError("coverage completeness contradicts retained verification evidence")
365
+ if result.coverage.input_execution_evidence_available != (
366
+ len(executions) == result.coverage.outputs_requested
367
+ ):
368
+ raise ValueError("input execution coverage contradicts captured executions")
369
+
370
+
371
+ def _input_contract(contract: Contract, declared: TableSpec) -> Contract:
372
+ output_name = "verify_input"
373
+ while output_name in {t.name for t in contract.inputs}:
374
+ output_name += "_input"
375
+ return Contract.model_validate(
376
+ {
377
+ **contract.model_dump(),
378
+ "output": TableSpec(name=output_name, schema_spec=declared.schema_spec),
379
+ "rules": (),
380
+ "preconditions": (),
381
+ }
382
+ )
383
+
384
+
385
+ def verify(
386
+ outputs: Mapping[str, VerificationInput],
387
+ *,
388
+ contract: Contract,
389
+ expected: ArtifactRef | ExecutionResult | None = None,
390
+ provenance: ExpectedProvenance | None = None,
391
+ reviews: tuple[IndependentReviewEvidence, ...] = (),
392
+ request_review: bool = False,
393
+ require_expected: bool = False,
394
+ require_rules: bool = False,
395
+ snapshots: Mapping[str, ArtifactRef] | None = None,
396
+ plan_sha256: str | None = None,
397
+ policy: ComparisonPolicy | None = None,
398
+ options: ComparisonOptions | None = None,
399
+ work_dir: Path | None = None,
400
+ ) -> VerificationResult:
401
+ """Capture and inspect complete outputs, preserving separate scientific O.
402
+
403
+ External artifact checks do not assert that an external ETL completed. Rules
404
+ require captured execution inputs. No engine, callback or plugin is executed.
405
+ """
406
+ contract = Contract.model_validate(contract)
407
+ if not outputs:
408
+ raise ValueError("verify requires at least one named result")
409
+ opts = ComparisonOptions.model_validate(ComparisonOptions() if options is None else options)
410
+ policy = ComparisonPolicy.model_validate(ComparisonPolicy() if policy is None else policy)
411
+ identifier = "verify_" + uuid4().hex
412
+ directory = (
413
+ local_output_directory(Path.cwd() / "equiframe-runs" if work_dir is None else work_dir)
414
+ / identifier
415
+ )
416
+ directory.mkdir(parents=True, exist_ok=False)
417
+ started = datetime.now(UTC).isoformat()
418
+ start = time.perf_counter()
419
+ captured: dict[str, ArtifactRef] = {}
420
+ executions: dict[str, ExecutionResult] = {}
421
+ errors: list[VerificationCheck] = []
422
+ technical_checks: list[VerificationCheck] = []
423
+
424
+ def capture(ref: ArtifactRef) -> ArtifactRef:
425
+ value = snapshot(ref, directory, opts, lambda: None)
426
+ read_table(value)
427
+ return value
428
+
429
+ def table(t: TableArtifact) -> TableArtifact:
430
+ copied = TableArtifact.model_validate(
431
+ {
432
+ **t.model_dump(),
433
+ "data": capture(t.data),
434
+ "index": capture(t.index) if t.index else None,
435
+ }
436
+ )
437
+ verify_table_metadata(copied)
438
+ return copied
439
+
440
+ for name, value in outputs.items():
441
+ _check("passed", "Output name validated", name)
442
+ if isinstance(value, PipelineResult):
443
+ value = PipelineResult.model_validate(value)
444
+ if value.execution is None:
445
+ technical_checks.append(_check("not_evaluable", "Pipeline has no execution", name))
446
+ continue
447
+ value = value.execution
448
+ if isinstance(value, ExecutionResult):
449
+ value = ExecutionResult.model_validate(value)
450
+ if value.plan.contract != contract:
451
+ raise ValueError("execution and verification contracts differ")
452
+ technical_checks.append(
453
+ _check(
454
+ "passed" if value.state == "completed" else "not_evaluable",
455
+ "Captured execution state: " + value.state,
456
+ name,
457
+ )
458
+ )
459
+ try:
460
+ output = table(value.output) if value.output else None
461
+ inputs = tuple(table(t) for t in value.inputs)
462
+ execution = ExecutionResult.model_validate(
463
+ {**value.model_dump(), "inputs": inputs, "output": output}
464
+ )
465
+ executions[name] = execution
466
+ if output:
467
+ captured[name] = output.data
468
+ except Exception as error:
469
+ technical_checks[-1] = _check(
470
+ "not_evaluable", "Execution evidence could not be captured", name
471
+ )
472
+ errors.append(
473
+ _check("failed", report_failure(error, "execution_capture_failed"), name)
474
+ )
475
+ else:
476
+ value = ArtifactRef.model_validate(value)
477
+ technical_checks.append(
478
+ _check(
479
+ "not_requested",
480
+ "External execution status was not supplied; artifact inspection only",
481
+ name,
482
+ )
483
+ )
484
+ try:
485
+ captured[name] = capture(value)
486
+ except Exception as error:
487
+ errors.append(
488
+ _check("failed", report_failure(error, "artifact_capture_failed"), name)
489
+ )
490
+ fixed_expected = None
491
+ expected_engine = None
492
+ if isinstance(expected, ExecutionResult):
493
+ expected = ExecutionResult.model_validate(expected)
494
+ expected_engine = expected.plan.engine
495
+ if provenance is not None:
496
+ provenance = ExpectedProvenance.model_validate(
497
+ {
498
+ **provenance.model_dump(),
499
+ "derived_from_engines": tuple(
500
+ dict.fromkeys((*provenance.derived_from_engines, expected_engine))
501
+ ),
502
+ }
503
+ )
504
+ if expected.state != "completed" or expected.output is None:
505
+ errors.append(_check("not_evaluable", "Expected execution did not complete"))
506
+ expected = None
507
+ require_expected = True
508
+ else:
509
+ expected = expected.output.data
510
+ if expected is not None:
511
+ try:
512
+ fixed_expected = capture(expected)
513
+ except Exception as error:
514
+ errors.append(_check("failed", report_failure(error, "expected_capture_failed")))
515
+ fixed_snapshots = {name: capture(ref) for name, ref in (snapshots or {}).items()}
516
+ if not fixed_snapshots and executions:
517
+ first = next(iter(executions.values()))
518
+ fixed_snapshots = {t.name: t.data for t in first.inputs}
519
+ identities = tuple((t.name, t.data.sha256) for t in first.inputs)
520
+ if any(
521
+ tuple((t.name, t.data.sha256) for t in e.inputs) != identities
522
+ for e in executions.values()
523
+ ):
524
+ errors.append(_check("failed", "Implementations received different input snapshots"))
525
+ if fixed_snapshots and executions:
526
+ identity = sorted((name, ref.sha256) for name, ref in fixed_snapshots.items())
527
+ if any(
528
+ sorted((t.name, t.data.sha256) for t in e.inputs) != identity
529
+ for e in executions.values()
530
+ ):
531
+ errors.append(_check("failed", "Execution inputs differ from the bound snapshots"))
532
+ binding = VerificationBinding.model_validate(
533
+ dict(
534
+ contract_sha256=contract_fingerprint(contract),
535
+ snapshots=tuple((name, ref.sha256) for name, ref in fixed_snapshots.items()),
536
+ plan_sha256=plan_sha256,
537
+ outputs=tuple((name, ref.sha256) for name, ref in captured.items()),
538
+ expected_sha256=fixed_expected.sha256 if fixed_expected else None,
539
+ )
540
+ )
541
+ comparisons: list[ComparisonResult] = []
542
+ input_comparisons: list[ComparisonResult] = []
543
+ self_checks: list[VerificationCheck] = []
544
+ for name, ref in fixed_snapshots.items():
545
+ declared = next((t for t in contract.inputs if t.name == name), None)
546
+ if declared is None:
547
+ raise ValueError("snapshot is not a declared contract input")
548
+ input_contract = _input_contract(contract, declared)
549
+ c = compare(
550
+ ref, ref, contract=input_contract, policy=policy, options=opts, work_dir=directory
551
+ )
552
+ input_comparisons.append(c)
553
+ self_checks.append(_comparison_check(c))
554
+ for name, ref in captured.items():
555
+ value = executions.get(name, ref)
556
+ c = compare(
557
+ value, value, contract=contract, policy=policy, options=opts, work_dir=directory
558
+ )
559
+ comparisons.append(c)
560
+ self_checks.append(_comparison_check(c, name))
561
+ peer_checks: list[VerificationCheck] = []
562
+ for (a, left), (b, right) in combinations(captured.items(), 2):
563
+ c = compare(
564
+ executions.get(a, left),
565
+ executions.get(b, right),
566
+ contract=contract,
567
+ policy=policy,
568
+ options=opts,
569
+ work_dir=directory,
570
+ )
571
+ comparisons.append(c)
572
+ peer_checks.append(_comparison_check(c))
573
+ expected_checks: list[VerificationCheck] = []
574
+ for name in outputs:
575
+ if fixed_expected is not None and name in captured:
576
+ c = compare(
577
+ executions.get(name, captured[name]),
578
+ fixed_expected,
579
+ contract=contract,
580
+ policy=policy,
581
+ options=opts,
582
+ reference="expected",
583
+ work_dir=directory,
584
+ )
585
+ comparisons.append(c)
586
+ expected_checks.append(_comparison_check(c, name))
587
+ else:
588
+ requested = expected is not None or require_expected
589
+ expected_checks.append(
590
+ _check(
591
+ "not_evaluable" if requested else "not_requested",
592
+ "Expected comparison unavailable" if requested else "No expected requested",
593
+ name,
594
+ )
595
+ )
596
+ rules: list[VerificationCheck] = []
597
+ evaluations: list[RuleEvaluation] = []
598
+ for name in outputs:
599
+ if name in executions and (contract.rules or require_rules):
600
+ evaluation = evaluate_rules(executions[name], work_dir=directory)
601
+ evaluations.append(evaluation)
602
+ rules.append(
603
+ _check(
604
+ "failed"
605
+ if evaluation.decision == "alert"
606
+ else "passed"
607
+ if evaluation.complete
608
+ else "not_evaluable",
609
+ "Selected rules: " + evaluation.decision,
610
+ name,
611
+ )
612
+ )
613
+ else:
614
+ rules.append(
615
+ _check(
616
+ "not_evaluable" if require_rules else "not_requested",
617
+ "Rules require captured execution inputs"
618
+ if require_rules
619
+ else "No operational rules requested for this artifact",
620
+ name,
621
+ )
622
+ )
623
+ requested_count = len(outputs) + len(outputs) * (len(outputs) - 1) // 2 + len(fixed_snapshots)
624
+ if expected is not None or require_expected:
625
+ requested_count += len(outputs)
626
+ complete = (
627
+ len(captured) == len(outputs)
628
+ and not errors
629
+ and len(comparisons) + len(input_comparisons) == requested_count
630
+ and all(c.complete for c in (*comparisons, *input_comparisons))
631
+ )
632
+ result = VerificationResult(
633
+ verification_id=identifier,
634
+ contract=contract,
635
+ binding=binding,
636
+ outputs=tuple(captured.items()),
637
+ expected_artifact=fixed_expected,
638
+ expected_source_engine=expected_engine,
639
+ comparison_policy=policy,
640
+ comparison_options=opts,
641
+ input_artifacts=tuple(fixed_snapshots.items()),
642
+ expected_required=require_expected,
643
+ rules_required=require_rules,
644
+ review_requested=request_review,
645
+ technical_outcomes=tuple(technical_checks),
646
+ provenance=provenance,
647
+ review_evidence=reviews,
648
+ inputs=_aggregate(tuple(self_checks), "Complete output schema, keys and value constraints")
649
+ if len(captured) == len(outputs)
650
+ else _check("not_evaluable", "Output capture incomplete"),
651
+ technical=_aggregate(tuple(technical_checks), "Technical outcomes retained separately")
652
+ if any(c.state != "not_requested" for c in technical_checks)
653
+ else _check("not_requested", "Artifact inspection does not assert ETL execution"),
654
+ integrity=_aggregate(tuple(errors), "Captured bytes verified against SHA-256 and size"),
655
+ agreement=_aggregate(tuple(peer_checks), "All requested pairs compared independently")
656
+ if len(outputs) > 1 and len(captured) == len(outputs)
657
+ else _check(
658
+ "not_requested" if len(outputs) == 1 else "not_evaluable", "Peer comparison scope"
659
+ ),
660
+ expected=tuple(expected_checks),
661
+ rules=tuple(rules),
662
+ coverage=VerificationCoverage(
663
+ outputs_requested=len(outputs),
664
+ outputs_inspected=len(captured),
665
+ comparisons_requested=requested_count,
666
+ comparisons_completed=sum(c.complete for c in (*comparisons, *input_comparisons)),
667
+ input_execution_evidence_available=len(executions) == len(outputs),
668
+ complete=complete,
669
+ ),
670
+ independent_review=_review(
671
+ binding, provenance, reviews, tuple(expected_checks), request_review
672
+ ),
673
+ comparisons=tuple(comparisons),
674
+ input_comparisons=tuple(input_comparisons),
675
+ executions=tuple(executions.items()),
676
+ rule_evaluations=tuple(evaluations),
677
+ started_at=started,
678
+ elapsed_seconds=time.perf_counter() - start,
679
+ )
680
+ publish_json(verification_summary(result), directory / "report.json")
681
+ (directory / "report.html").write_text(render_verification_report(result), encoding="utf-8")
682
+ result = VerificationResult.model_validate(
683
+ {**result.model_dump(), "evidence": inventory(directory)}
684
+ )
685
+ publish_json(result, directory / "verification.json")
686
+ return result
687
+
688
+
689
+ def verification_summary(result: VerificationResult) -> dict[str, object]:
690
+ return verification_summary_v1(result)
691
+
692
+
693
+ def render_verification_report(result: VerificationResult) -> str:
694
+ return verification_html_v1(result)
695
+
696
+
697
+ def read_verification(path: Path) -> VerificationResult:
698
+ path = path / "verification.json" if path.is_dir() else path
699
+ result = load_json(VerificationResult, path)
700
+ directory = path.parent.resolve()
701
+ verify_files(directory, result.evidence)
702
+ actual = {
703
+ p.relative_to(directory).as_posix()
704
+ for p in directory.rglob("*")
705
+ if p.is_file() and p != path.resolve()
706
+ }
707
+ if actual != {f.path for f in result.evidence}:
708
+ raise ArtifactIntegrityError("verification inventory is not closed")
709
+ listed = {f.path: f for f in result.evidence}
710
+ refs = [ref for _, ref in result.outputs]
711
+ refs.extend(ref for _, ref in result.input_artifacts)
712
+ if result.expected_artifact:
713
+ refs.append(result.expected_artifact)
714
+ for _, e in result.executions:
715
+ for t in (*e.inputs, *((e.output,) if e.output else ())):
716
+ refs.extend((t.data, *((t.index,) if t.index else ())))
717
+ for r in result.rule_evaluations:
718
+ refs.extend(c.findings for c in (*r.preconditions, *r.rules) if c.findings)
719
+ for c in (*result.comparisons, *result.input_comparisons):
720
+ refs.append(c.differences)
721
+ refs.extend(s.data for s in (c.left, c.right) if s is not None)
722
+ for ref in refs:
723
+ target = local_path(ref).resolve()
724
+ if not target.is_relative_to(directory):
725
+ raise ArtifactIntegrityError("verification artifact escapes inventory")
726
+ item = listed.get(target.relative_to(directory).as_posix())
727
+ if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
728
+ raise ArtifactIntegrityError("verification artifact contradicts inventory")
729
+ for _, e in result.executions:
730
+ for t in (*e.inputs, *((e.output,) if e.output else ())):
731
+ verify_table_metadata(t)
732
+ for r in result.rule_evaluations:
733
+ if read_rule_evaluation(directory / r.evaluation_id / "rules.json") != r:
734
+ raise ArtifactIntegrityError(
735
+ "selected rule evaluation contradicts verification manifest"
736
+ )
737
+ for check in (*r.preconditions, *r.rules):
738
+ if check.findings:
739
+ count = 0
740
+ for finding in iter_rule_findings(check.findings):
741
+ if count < len(check.examples) and finding != check.examples[count]:
742
+ raise ArtifactIntegrityError("rule examples contradict complete findings")
743
+ count += 1
744
+ if count != check.violation_count:
745
+ raise ArtifactIntegrityError("rule findings contradict recorded counts")
746
+ for c in (*result.comparisons, *result.input_comparisons):
747
+ if read_comparison(directory / c.comparison_id / "comparison.json") != c:
748
+ raise ArtifactIntegrityError("verification comparison contradicts manifest")
749
+ count = violations = 0
750
+ for difference in iter_differences(c.differences):
751
+ if count < len(c.examples) and difference != c.examples[count]:
752
+ raise ArtifactIntegrityError("comparison examples contradict complete differences")
753
+ count += 1
754
+ violations += int(difference.category in ("schema", "contract"))
755
+ if count != c.counts.difference_records or violations != c.counts.contract_violations:
756
+ raise ArtifactIntegrityError("difference counts contradict complete evidence")
757
+ if len(c.examples) != min(count, c.options.max_examples):
758
+ raise ArtifactIntegrityError("comparison examples contradict the configured budget")
759
+ if parse_json((directory / "report.json").read_bytes()) != verification_summary_v1(result):
760
+ raise ArtifactIntegrityError("verification report contradicts manifest")
761
+ if (directory / "report.html").read_bytes() != verification_html_v1(result).encode("utf-8"):
762
+ raise ArtifactIntegrityError("verification HTML contradicts manifest")
763
+ return result
764
+
765
+
766
+ def verify_migration(
767
+ path: Path,
768
+ *,
769
+ work_dir: Path | None = None,
770
+ provenance: ExpectedProvenance | None = None,
771
+ reviews: tuple[IndependentReviewEvidence, ...] = (),
772
+ request_review: bool = False,
773
+ ) -> VerificationResult:
774
+ from equiframe.migration.io import read_migration
775
+ from equiframe.migration.validation import ir_fingerprint
776
+
777
+ result = read_migration(path)
778
+ outputs = {
779
+ name: p
780
+ for name, p in (("source", result.source), ("target", result.target))
781
+ if p is not None
782
+ }
783
+ if len(outputs) != 2:
784
+ raise ValueError("migration has no two captured pipelines to verify")
785
+ return verify(
786
+ outputs,
787
+ contract=result.spec.contract,
788
+ expected=result.expected,
789
+ require_expected=result.expected_requested,
790
+ require_rules=result.spec.require_rules,
791
+ provenance=provenance,
792
+ reviews=reviews,
793
+ request_review=request_review,
794
+ snapshots={t.name: t.data for t in result.inputs},
795
+ plan_sha256=ir_fingerprint(result.spec.ir),
796
+ policy=result.spec.comparison_policy,
797
+ options=result.spec.comparison_options,
798
+ work_dir=work_dir,
799
+ )