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
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Strict migration configuration and evidence; comparisons remain independent of R/M/B/H."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, model_validator
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from equiframe._base import (
|
|
11
|
+
FrozenModel,
|
|
12
|
+
Identifier,
|
|
13
|
+
NonNegativeInt,
|
|
14
|
+
PositiveInt,
|
|
15
|
+
Text,
|
|
16
|
+
_ExplicitOutcome,
|
|
17
|
+
)
|
|
18
|
+
from equiframe.comparison_types import ComparisonOptions, ComparisonPolicy, ComparisonResult
|
|
19
|
+
from equiframe.contracts import Contract
|
|
20
|
+
from equiframe.execution_types import ExecutionOptions, TableArtifact
|
|
21
|
+
from equiframe.pipeline_types import EvidenceFile, PipelineInput, PipelineResult, StageState
|
|
22
|
+
from equiframe.profile_types import ProfileOptions, ProfileSpec, profile_digest
|
|
23
|
+
from equiframe.rule_types import RuleOptions
|
|
24
|
+
from equiframe.serialization import contract_fingerprint
|
|
25
|
+
from equiframe.types import ArtifactRef, Sha256
|
|
26
|
+
|
|
27
|
+
from .types import CompilationCheck, RelationalPlan
|
|
28
|
+
from .validation import ir_fingerprint
|
|
29
|
+
|
|
30
|
+
MigrationStageName = Literal[
|
|
31
|
+
"preflight",
|
|
32
|
+
"capture",
|
|
33
|
+
"source",
|
|
34
|
+
"target",
|
|
35
|
+
"comparison",
|
|
36
|
+
"verification_source",
|
|
37
|
+
"verification_target",
|
|
38
|
+
]
|
|
39
|
+
MIGRATION_STAGES: tuple[MigrationStageName, ...] = (
|
|
40
|
+
"preflight",
|
|
41
|
+
"capture",
|
|
42
|
+
"source",
|
|
43
|
+
"target",
|
|
44
|
+
"comparison",
|
|
45
|
+
"verification_source",
|
|
46
|
+
"verification_target",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MigrationOptions(FrozenModel):
|
|
51
|
+
timeout_seconds: Annotated[float, Field(gt=0)] | None = None
|
|
52
|
+
max_capture_rows: NonNegativeInt = 10_000_000
|
|
53
|
+
max_capture_bytes: PositiveInt = 512 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MigrationSpec(FrozenModel):
|
|
57
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
58
|
+
ir: RelationalPlan
|
|
59
|
+
contract: Contract
|
|
60
|
+
source_engine: Identifier
|
|
61
|
+
target_engine: Identifier
|
|
62
|
+
require_rules: bool = True
|
|
63
|
+
execution_options: ExecutionOptions = Field(default_factory=ExecutionOptions)
|
|
64
|
+
comparison_policy: ComparisonPolicy = Field(default_factory=ComparisonPolicy)
|
|
65
|
+
comparison_options: ComparisonOptions = Field(default_factory=ComparisonOptions)
|
|
66
|
+
rule_options: RuleOptions = Field(default_factory=RuleOptions)
|
|
67
|
+
profile: ProfileSpec | None = None
|
|
68
|
+
profile_options: ProfileOptions = Field(default_factory=ProfileOptions)
|
|
69
|
+
options: MigrationOptions = Field(default_factory=MigrationOptions)
|
|
70
|
+
|
|
71
|
+
@model_validator(mode="after")
|
|
72
|
+
def consistent(self) -> Self:
|
|
73
|
+
if self.source_engine == self.target_engine:
|
|
74
|
+
raise ValueError("migration requires two distinct engines")
|
|
75
|
+
if self.profile and self.profile.family != self.contract.family:
|
|
76
|
+
raise ValueError("migration profile family must match the contract")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MigrationRequest(FrozenModel):
|
|
81
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
82
|
+
spec: MigrationSpec
|
|
83
|
+
inputs: tuple[PipelineInput, ...]
|
|
84
|
+
expected: ArtifactRef | None = None
|
|
85
|
+
|
|
86
|
+
@model_validator(mode="after")
|
|
87
|
+
def consistent(self) -> Self:
|
|
88
|
+
names = tuple(i.name for i in self.inputs)
|
|
89
|
+
if len(set(names)) != len(names) or set(names) != {
|
|
90
|
+
t.name for t in self.spec.contract.inputs
|
|
91
|
+
}:
|
|
92
|
+
raise ValueError("migration inputs must exactly cover declared tables")
|
|
93
|
+
if any(i.artifact.format not in ("arrow", "parquet") for i in self.inputs):
|
|
94
|
+
raise ValueError("migration inputs require Arrow or Parquet")
|
|
95
|
+
if self.expected and self.expected.format not in ("arrow", "parquet"):
|
|
96
|
+
raise ValueError("migration expected requires Arrow or Parquet")
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CompilerRecord(FrozenModel):
|
|
101
|
+
engine: Identifier
|
|
102
|
+
transformation_id: Identifier
|
|
103
|
+
revision: PositiveInt
|
|
104
|
+
code_version: Text
|
|
105
|
+
ir_sha256: Sha256
|
|
106
|
+
contract_sha256: Sha256
|
|
107
|
+
check: CompilationCheck
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def pipeline_complete(spec: MigrationSpec, result: PipelineResult) -> bool:
|
|
111
|
+
"""Completion of explicitly required components; optional R never implies no-alert."""
|
|
112
|
+
if spec.require_rules:
|
|
113
|
+
return result.complete
|
|
114
|
+
required = {"execution"}
|
|
115
|
+
if spec.profile is not None:
|
|
116
|
+
required.add("profile")
|
|
117
|
+
if result.detection_requested:
|
|
118
|
+
required.add("detection")
|
|
119
|
+
return not result.cancelled and all(
|
|
120
|
+
s.state == "completed" for s in result.stages if s.stage in required
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class MigrationStage(FrozenModel):
|
|
125
|
+
stage: MigrationStageName
|
|
126
|
+
state: StageState
|
|
127
|
+
code: Identifier
|
|
128
|
+
elapsed_seconds: Annotated[float, Field(ge=0)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _identity(t: TableArtifact) -> tuple[object, ...]:
|
|
132
|
+
return (
|
|
133
|
+
t.name,
|
|
134
|
+
t.data.sha256,
|
|
135
|
+
t.data.size_bytes,
|
|
136
|
+
(t.index.sha256, t.index.size_bytes) if t.index else None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class MigrationResult(_ExplicitOutcome, FrozenModel):
|
|
141
|
+
_truth_value_guidance = "inspect .state, .verification, .complete and .exit_code explicitly"
|
|
142
|
+
|
|
143
|
+
schema_version: Literal["1.0"] = "1.0"
|
|
144
|
+
migration_id: Identifier
|
|
145
|
+
spec: MigrationSpec
|
|
146
|
+
spec_sha256: Sha256
|
|
147
|
+
started_at: Text
|
|
148
|
+
finished_at: Text
|
|
149
|
+
elapsed_seconds: Annotated[float, Field(ge=0)]
|
|
150
|
+
stages: tuple[MigrationStage, ...]
|
|
151
|
+
compilers: tuple[CompilerRecord, ...]
|
|
152
|
+
inputs: tuple[TableArtifact, ...]
|
|
153
|
+
expected_requested: bool
|
|
154
|
+
expected: ArtifactRef | None
|
|
155
|
+
detection_requested: bool
|
|
156
|
+
source: PipelineResult | None
|
|
157
|
+
target: PipelineResult | None
|
|
158
|
+
comparison: ComparisonResult | None
|
|
159
|
+
verification_source: ComparisonResult | None
|
|
160
|
+
verification_target: ComparisonResult | None
|
|
161
|
+
cancelled: bool
|
|
162
|
+
evidence: tuple[EvidenceFile, ...]
|
|
163
|
+
event_count: Annotated[int, Field(ge=2)]
|
|
164
|
+
event_head_sha256: Sha256
|
|
165
|
+
|
|
166
|
+
@model_validator(mode="after")
|
|
167
|
+
def consistent(self) -> Self:
|
|
168
|
+
if self.spec_sha256 != profile_digest(self.spec.model_dump(mode="json")):
|
|
169
|
+
raise ValueError("migration specification fingerprint mismatch")
|
|
170
|
+
if tuple(s.stage for s in self.stages) != MIGRATION_STAGES:
|
|
171
|
+
raise ValueError("migration stages must occur once in the declared order")
|
|
172
|
+
if len({f.path for f in self.evidence}) != len(self.evidence):
|
|
173
|
+
raise ValueError("migration evidence paths must be unique")
|
|
174
|
+
if self.expected and (
|
|
175
|
+
not self.expected_requested or self.expected.format not in ("arrow", "parquet")
|
|
176
|
+
):
|
|
177
|
+
raise ValueError("unexpected or invalid expected reference")
|
|
178
|
+
if self.detection_requested and self.spec.profile is None:
|
|
179
|
+
raise ValueError("migration detection requires an explicit profile")
|
|
180
|
+
names = tuple(t.name for t in self.inputs)
|
|
181
|
+
declared = {t.name for t in self.spec.contract.inputs}
|
|
182
|
+
if len(set(names)) != len(names) or not set(names) <= declared:
|
|
183
|
+
raise ValueError("migration captured inputs must be unique declared tables")
|
|
184
|
+
if self.stages[1].state == "completed" and (
|
|
185
|
+
self.stages[0].state != "completed"
|
|
186
|
+
or set(names) != declared
|
|
187
|
+
or (self.expected_requested and self.expected is None)
|
|
188
|
+
):
|
|
189
|
+
raise ValueError("complete capture requires all requested data")
|
|
190
|
+
if self.compilers and tuple(c.engine for c in self.compilers) != (
|
|
191
|
+
self.spec.source_engine,
|
|
192
|
+
self.spec.target_engine,
|
|
193
|
+
):
|
|
194
|
+
raise ValueError("compiler records must bind the configured engine pair")
|
|
195
|
+
if self.stages[0].state == "completed" and len(self.compilers) != 2:
|
|
196
|
+
raise ValueError("preflight requires both compiler records")
|
|
197
|
+
for c in self.compilers:
|
|
198
|
+
if (
|
|
199
|
+
not c.check.supported
|
|
200
|
+
or c.check.engine != c.engine
|
|
201
|
+
or c.ir_sha256 != ir_fingerprint(self.spec.ir)
|
|
202
|
+
or c.contract_sha256 != contract_fingerprint(self.spec.contract)
|
|
203
|
+
):
|
|
204
|
+
raise ValueError("compiler record contradicts the plan")
|
|
205
|
+
children = (self.source, self.target)
|
|
206
|
+
for n, p in enumerate(children):
|
|
207
|
+
stage = self.stages[n + 2]
|
|
208
|
+
if p is None:
|
|
209
|
+
if stage.state in ("completed", "not_evaluable"):
|
|
210
|
+
raise ValueError("migration stage requires its pipeline")
|
|
211
|
+
continue
|
|
212
|
+
if len(self.compilers) != 2 or self.stages[1].state != "completed":
|
|
213
|
+
raise ValueError("pipeline requires preflight and fixed capture")
|
|
214
|
+
record = self.compilers[n]
|
|
215
|
+
e = p.spec.execution
|
|
216
|
+
opts = e.options.model_copy(
|
|
217
|
+
update={
|
|
218
|
+
"timeout_seconds": self.spec.execution_options.timeout_seconds,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
if (
|
|
222
|
+
e.contract != self.spec.contract
|
|
223
|
+
or e.engine != record.engine
|
|
224
|
+
or e.transformation_id != record.transformation_id
|
|
225
|
+
or e.transformation_revision != record.revision
|
|
226
|
+
or e.parameters
|
|
227
|
+
or opts != self.spec.execution_options
|
|
228
|
+
or p.spec.profile != self.spec.profile
|
|
229
|
+
or p.spec.rule_options != self.spec.rule_options
|
|
230
|
+
or p.spec.profile_options != self.spec.profile_options
|
|
231
|
+
or p.detection_requested != self.detection_requested
|
|
232
|
+
or p.comparison_requested
|
|
233
|
+
or p.spec.comparison_policy != self.spec.comparison_policy
|
|
234
|
+
or p.spec.comparison_options != self.spec.comparison_options
|
|
235
|
+
):
|
|
236
|
+
raise ValueError("pipeline contradicts the migration configuration")
|
|
237
|
+
timeout = e.options.timeout_seconds
|
|
238
|
+
declared_timeout = self.spec.execution_options.timeout_seconds
|
|
239
|
+
if timeout is None and (
|
|
240
|
+
declared_timeout is not None or self.spec.options.timeout_seconds is not None
|
|
241
|
+
):
|
|
242
|
+
raise ValueError("effective execution must retain the declared timeout")
|
|
243
|
+
if timeout is not None and (
|
|
244
|
+
timeout <= 0
|
|
245
|
+
or (declared_timeout is not None and timeout > declared_timeout)
|
|
246
|
+
or (
|
|
247
|
+
self.spec.options.timeout_seconds is not None
|
|
248
|
+
and timeout > self.spec.options.timeout_seconds
|
|
249
|
+
)
|
|
250
|
+
):
|
|
251
|
+
raise ValueError("effective execution timeout exceeds its declared budget")
|
|
252
|
+
if (stage.state == "completed") != pipeline_complete(self.spec, p):
|
|
253
|
+
raise ValueError("migration stage contradicts pipeline completion")
|
|
254
|
+
if stage.state in ("skipped", "failed"):
|
|
255
|
+
raise ValueError("failed or skipped stage cannot claim a pipeline")
|
|
256
|
+
if p.execution is not None:
|
|
257
|
+
if (
|
|
258
|
+
p.execution.implementation_version is not None
|
|
259
|
+
and p.execution.implementation_version != record.code_version
|
|
260
|
+
):
|
|
261
|
+
raise ValueError("execution compiler version mismatch")
|
|
262
|
+
if p.execution.state == "completed" and sorted(
|
|
263
|
+
(_identity(t) for t in p.execution.inputs), key=lambda x: str(x[0])
|
|
264
|
+
) != sorted((_identity(t) for t in self.inputs), key=lambda x: str(x[0])):
|
|
265
|
+
raise ValueError("pipeline inputs differ from the single fixed capture")
|
|
266
|
+
comparisons = (self.comparison, self.verification_source, self.verification_target)
|
|
267
|
+
for n, cmp in enumerate(comparisons):
|
|
268
|
+
stage = self.stages[n + 4]
|
|
269
|
+
if cmp is None:
|
|
270
|
+
if stage.state in ("completed", "not_evaluable"):
|
|
271
|
+
raise ValueError("comparison stage requires its result")
|
|
272
|
+
continue
|
|
273
|
+
if stage.state in ("skipped", "failed"):
|
|
274
|
+
raise ValueError("failed or skipped stage cannot claim a comparison")
|
|
275
|
+
if (stage.state == "completed") != (cmp.complete and cmp.state != "not_evaluable"):
|
|
276
|
+
raise ValueError("comparison stage contradicts evaluability")
|
|
277
|
+
if (
|
|
278
|
+
cmp.spec.contract != self.spec.contract
|
|
279
|
+
or cmp.spec.policy != self.spec.comparison_policy
|
|
280
|
+
or cmp.options.model_copy(
|
|
281
|
+
update={
|
|
282
|
+
"timeout_seconds": self.spec.comparison_options.timeout_seconds,
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
!= self.spec.comparison_options
|
|
286
|
+
):
|
|
287
|
+
raise ValueError("migration comparison configuration mismatch")
|
|
288
|
+
timeout = cmp.options.timeout_seconds
|
|
289
|
+
declared_timeout = self.spec.comparison_options.timeout_seconds
|
|
290
|
+
if timeout is None and (
|
|
291
|
+
declared_timeout is not None or self.spec.options.timeout_seconds is not None
|
|
292
|
+
):
|
|
293
|
+
raise ValueError("effective comparison must retain the declared timeout")
|
|
294
|
+
if timeout is not None and (
|
|
295
|
+
(declared_timeout is not None and timeout > declared_timeout)
|
|
296
|
+
or (
|
|
297
|
+
self.spec.options.timeout_seconds is not None
|
|
298
|
+
and timeout > self.spec.options.timeout_seconds
|
|
299
|
+
)
|
|
300
|
+
):
|
|
301
|
+
raise ValueError("effective comparison exceeds its declared timeout")
|
|
302
|
+
left = self.source if n != 2 else self.target
|
|
303
|
+
if (
|
|
304
|
+
left is None
|
|
305
|
+
or left.execution is None
|
|
306
|
+
or (cmp.complete and cmp.left is None)
|
|
307
|
+
or (cmp.left is not None and cmp.left.execution != left.execution)
|
|
308
|
+
):
|
|
309
|
+
raise ValueError("migration comparison left execution mismatch")
|
|
310
|
+
if cmp.left is not None and (
|
|
311
|
+
cmp.left.table != left.execution.output
|
|
312
|
+
or left.execution.output is None
|
|
313
|
+
or (cmp.left.data.sha256, cmp.left.data.size_bytes, cmp.left.data.format)
|
|
314
|
+
!= (
|
|
315
|
+
left.execution.output.data.sha256,
|
|
316
|
+
left.execution.output.data.size_bytes,
|
|
317
|
+
left.execution.output.data.format,
|
|
318
|
+
)
|
|
319
|
+
):
|
|
320
|
+
raise ValueError("comparison snapshot differs from the native output")
|
|
321
|
+
if n == 0:
|
|
322
|
+
if (
|
|
323
|
+
cmp.reference != "peer"
|
|
324
|
+
or self.target is None
|
|
325
|
+
or self.target.execution is None
|
|
326
|
+
or (cmp.complete and cmp.right is None)
|
|
327
|
+
or (cmp.right is not None and cmp.right.execution != self.target.execution)
|
|
328
|
+
):
|
|
329
|
+
raise ValueError("peer comparison must bind both native executions")
|
|
330
|
+
if cmp.right is not None and (
|
|
331
|
+
cmp.right.table != self.target.execution.output
|
|
332
|
+
or self.target.execution.output is None
|
|
333
|
+
or (cmp.right.data.sha256, cmp.right.data.size_bytes, cmp.right.data.format)
|
|
334
|
+
!= (
|
|
335
|
+
self.target.execution.output.data.sha256,
|
|
336
|
+
self.target.execution.output.data.size_bytes,
|
|
337
|
+
self.target.execution.output.data.format,
|
|
338
|
+
)
|
|
339
|
+
):
|
|
340
|
+
raise ValueError("peer snapshot differs from the native output")
|
|
341
|
+
elif (
|
|
342
|
+
not self.expected_requested
|
|
343
|
+
or self.expected is None
|
|
344
|
+
or cmp.reference != "expected"
|
|
345
|
+
or (cmp.complete and cmp.right is None)
|
|
346
|
+
or (
|
|
347
|
+
cmp.right is not None
|
|
348
|
+
and (cmp.right.data.sha256, cmp.right.data.size_bytes, cmp.right.data.format)
|
|
349
|
+
!= (self.expected.sha256, self.expected.size_bytes, self.expected.format)
|
|
350
|
+
)
|
|
351
|
+
):
|
|
352
|
+
raise ValueError("verification must bind the one captured expected")
|
|
353
|
+
return self
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def complete(self) -> bool:
|
|
357
|
+
required = set(MIGRATION_STAGES[:5])
|
|
358
|
+
if self.expected_requested:
|
|
359
|
+
required.update(MIGRATION_STAGES[5:])
|
|
360
|
+
return not self.cancelled and all(
|
|
361
|
+
s.state == "completed" for s in self.stages if s.stage in required
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def state(self) -> Literal["equivalent", "different", "not_evaluable"]:
|
|
366
|
+
return self.comparison.state if self.comparison is not None else "not_evaluable"
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def verification(self) -> Literal["correct", "divergent", "not_evaluable", "not_requested"]:
|
|
370
|
+
if not self.expected_requested:
|
|
371
|
+
return "not_requested"
|
|
372
|
+
checks = (self.verification_source, self.verification_target)
|
|
373
|
+
if any(c is None or not c.complete or c.state == "not_evaluable" for c in checks):
|
|
374
|
+
return "not_evaluable"
|
|
375
|
+
return (
|
|
376
|
+
"divergent"
|
|
377
|
+
if any(c is not None and c.state == "different" for c in checks)
|
|
378
|
+
else "correct"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def exit_code(self) -> int:
|
|
383
|
+
if any(
|
|
384
|
+
s.code in ("invalid_input", "unsupported_capability", "missing_dependency")
|
|
385
|
+
for s in self.stages[:2]
|
|
386
|
+
):
|
|
387
|
+
return 2
|
|
388
|
+
if not self.complete:
|
|
389
|
+
return 3
|
|
390
|
+
return int(
|
|
391
|
+
self.state == "different"
|
|
392
|
+
or self.verification == "divergent"
|
|
393
|
+
or any(
|
|
394
|
+
p is not None
|
|
395
|
+
and (
|
|
396
|
+
p.hybrid.r.decision == "alert"
|
|
397
|
+
or p.hybrid.result.decision == "alert"
|
|
398
|
+
or (
|
|
399
|
+
p.detection is not None
|
|
400
|
+
and any(d.result.decision == "alert" for d in p.detection.b)
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
for p in (self.source, self.target)
|
|
404
|
+
)
|
|
405
|
+
)
|