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.
- equiframe/__init__.py +832 -0
- equiframe/__main__.py +3 -0
- equiframe/_base.py +84 -0
- equiframe/_cli_support.py +125 -0
- equiframe/_comparison_io.py +161 -0
- equiframe/_dataset_provenance.py +146 -0
- equiframe/_decimal.py +22 -0
- equiframe/_diff_capture.py +433 -0
- equiframe/_doctor.py +123 -0
- equiframe/_document_io.py +61 -0
- equiframe/_documents.py +79 -0
- equiframe/_duckdb_transport.py +149 -0
- equiframe/_error_reporting.py +52 -0
- equiframe/_first_use.py +102 -0
- equiframe/_path_compat.py +26 -0
- equiframe/_physical.py +133 -0
- equiframe/_r_review.R +308 -0
- equiframe/_regular_io.py +167 -0
- equiframe/_report_format_v1.py +298 -0
- equiframe/_report_io.py +591 -0
- equiframe/_stability_inventory.py +1421 -0
- equiframe/_timezones.py +257 -0
- equiframe/_vendor/cyclonedx/LICENSE +201 -0
- equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
- equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
- equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
- equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
- equiframe/adapters.py +454 -0
- equiframe/assertion_types.py +122 -0
- equiframe/assertions.py +271 -0
- equiframe/benchmark.py +101 -0
- equiframe/bundle.py +240 -0
- equiframe/cli.py +693 -0
- equiframe/comparison.py +937 -0
- equiframe/comparison_report.py +660 -0
- equiframe/comparison_types.py +257 -0
- equiframe/compatibility.py +546 -0
- equiframe/contracts.py +879 -0
- equiframe/datasets.py +1301 -0
- equiframe/decision.py +34 -0
- equiframe/destinations.py +295 -0
- equiframe/detector_types.py +431 -0
- equiframe/detectors.py +389 -0
- equiframe/diff.py +192 -0
- equiframe/diff_contracts.py +523 -0
- equiframe/docs/adoption.md +329 -0
- equiframe/docs/api.md +534 -0
- equiframe/docs/arquitecto.md +143 -0
- equiframe/docs/choosing-api.md +84 -0
- equiframe/docs/cientifico.md +180 -0
- equiframe/docs/compatibility.md +254 -0
- equiframe/docs/contributing.md +171 -0
- equiframe/docs/dependencies.md +237 -0
- equiframe/docs/eda.md +166 -0
- equiframe/docs/extending.md +158 -0
- equiframe/docs/fast-ci.md +52 -0
- equiframe/docs/first-use.md +78 -0
- equiframe/docs/index.md +142 -0
- equiframe/docs/ingestion.md +145 -0
- equiframe/docs/inicio.md +129 -0
- equiframe/docs/installed-private.md +97 -0
- equiframe/docs/journeys.md +362 -0
- equiframe/docs/lectura-conversion.md +211 -0
- equiframe/docs/licensing.md +17 -0
- equiframe/docs/maintenance.md +233 -0
- equiframe/docs/migration-ir.md +333 -0
- equiframe/docs/option-audit.md +45 -0
- equiframe/docs/outcome-gates.md +134 -0
- equiframe/docs/performance.md +263 -0
- equiframe/docs/piloto-simulado.md +56 -0
- equiframe/docs/quickstart.md +156 -0
- equiframe/docs/r_review.md +212 -0
- equiframe/docs/recipes.md +248 -0
- equiframe/docs/releasing.md +183 -0
- equiframe/docs/security.md +218 -0
- equiframe/docs/semantics.md +274 -0
- equiframe/docs/sqlite-snapshots.md +262 -0
- equiframe/docs/universal-sources.md +49 -0
- equiframe/docs/universalidad-cinco-pasos.md +107 -0
- equiframe/docs/upgrading.md +538 -0
- equiframe/duckdb_capture.py +410 -0
- equiframe/eda/__init__.py +36 -0
- equiframe/eda/__main__.py +3 -0
- equiframe/eda/_backend.py +612 -0
- equiframe/eda/_inventory.py +49 -0
- equiframe/eda/_paths.py +49 -0
- equiframe/eda/_report.py +291 -0
- equiframe/eda/api.py +535 -0
- equiframe/eda/cli.py +102 -0
- equiframe/eda/models.py +239 -0
- equiframe/errors.py +83 -0
- equiframe/evidence.py +375 -0
- equiframe/examples/integrations/eda_generic.py +60 -0
- equiframe/examples/integrations/outcome_gates.py +94 -0
- equiframe/examples/integrations/sqlite_snapshot.py +246 -0
- equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
- equiframe/examples/integrations/universal_first_use.py +296 -0
- equiframe/examples/journeys/README.md +31 -0
- equiframe/examples/journeys/__init__.py +1 -0
- equiframe/examples/journeys/a_pandas_refactor.py +88 -0
- equiframe/examples/journeys/b_native_migration.py +181 -0
- equiframe/examples/journeys/c_retail_etl.py +460 -0
- equiframe/examples/journeys/common.py +136 -0
- equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
- equiframe/examples/journeys/e_telemetry.py +135 -0
- equiframe/examples/journeys/f_data_science_preparation.py +84 -0
- equiframe/examples/journeys/g_external_snapshots.py +197 -0
- equiframe/examples/journeys/h_portable_ci.py +129 -0
- equiframe/examples/journeys/requirements.lock +249 -0
- equiframe/examples/journeys/verify_journeys.py +117 -0
- equiframe/examples/load_convert_compare.py +85 -0
- equiframe/execution.py +355 -0
- equiframe/execution_types.py +177 -0
- equiframe/ingestion.py +744 -0
- equiframe/local.py +126 -0
- equiframe/migration/__init__.py +94 -0
- equiframe/migration/_physical.py +8 -0
- equiframe/migration/backends/_timestamp_transport.py +109 -0
- equiframe/migration/backends/duckdb.py +651 -0
- equiframe/migration/backends/duckdb_decimal.py +125 -0
- equiframe/migration/backends/duckdb_time.py +87 -0
- equiframe/migration/backends/pandas.py +544 -0
- equiframe/migration/backends/polars.py +915 -0
- equiframe/migration/backends/polars_decimal.py +231 -0
- equiframe/migration/io.py +198 -0
- equiframe/migration/ports.py +96 -0
- equiframe/migration/runner.py +545 -0
- equiframe/migration/runner_types.py +405 -0
- equiframe/migration/types.py +354 -0
- equiframe/migration/validation.py +619 -0
- equiframe/normalization.py +358 -0
- equiframe/pipeline.py +274 -0
- equiframe/pipeline_types.py +247 -0
- equiframe/ports.py +39 -0
- equiframe/preprocessing.py +110 -0
- equiframe/profile_types.py +343 -0
- equiframe/profiles.py +447 -0
- equiframe/py.typed +0 -0
- equiframe/r_review.py +706 -0
- equiframe/reduction.py +518 -0
- equiframe/registry.py +121 -0
- equiframe/report_rendering.py +366 -0
- equiframe/report_types.py +294 -0
- equiframe/reporting.py +26 -0
- equiframe/rule_types.py +178 -0
- equiframe/rules.py +819 -0
- equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
- equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
- equiframe/schemas/r-review-options-1.0.schema.json +38 -0
- equiframe/schemas/r-review-result-1.0.schema.json +207 -0
- equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
- equiframe/serialization.py +152 -0
- equiframe/snapshot.py +296 -0
- equiframe/sources.py +283 -0
- equiframe/tables.py +459 -0
- equiframe/types.py +71 -0
- equiframe/values.py +252 -0
- equiframe/verification_request.py +35 -0
- equiframe/verify.py +799 -0
- equiframe/verify_types.py +255 -0
- equiframe/workflow.py +510 -0
- equiframe/workflow_request.py +38 -0
- equiframe-0.12.4.dist-info/METADATA +268 -0
- equiframe-0.12.4.dist-info/RECORD +170 -0
- equiframe-0.12.4.dist-info/WHEEL +4 -0
- equiframe-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
- 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
|
+
)
|