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
@@ -0,0 +1,545 @@
1
+ """Compile and execute two native ETLs over one fixed, faithful input capture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable, Mapping
7
+ from datetime import datetime
8
+ from datetime import timezone as _timezone
9
+ from functools import partial
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Literal, TypeVar
12
+ from uuid import uuid4
13
+
14
+ from typing_extensions import TypeAliasType
15
+
16
+ from equiframe._comparison_io import snapshot
17
+ from equiframe._duckdb_transport import duckdb_temporal_capabilities
18
+ from equiframe._physical import physical_size
19
+ from equiframe._regular_io import local_output_directory
20
+ from equiframe.adapters import _input_capabilities, _timestamp_codec, native_adapter
21
+ from equiframe.comparison import compare
22
+ from equiframe.comparison_types import ComparisonResult
23
+ from equiframe.detector_types import DetectorBundle
24
+ from equiframe.errors import (
25
+ CapabilityError,
26
+ ExecutionCancelled,
27
+ ExecutionTimeout,
28
+ InvalidExecutionInput,
29
+ MissingDependencyError,
30
+ ResourceLimitError,
31
+ )
32
+ from equiframe.evidence import EventJournal, inventory, publish_json
33
+ from equiframe.execution import CancellationToken, RunContext, _contract_capabilities
34
+ from equiframe.execution_types import ExecutionPlan, TableArtifact
35
+ from equiframe.local import LocalArtifactStore
36
+ from equiframe.normalization import logical_signature, normalize, physical_value
37
+ from equiframe.pipeline import run_pipeline
38
+ from equiframe.pipeline_types import PipelineResult, PipelineSpec
39
+ from equiframe.ports import DataSource, EngineAdapter
40
+ from equiframe.profile_types import profile_digest
41
+ from equiframe.registry import TransformationRegistry
42
+ from equiframe.rules import RuleRegistry
43
+ from equiframe.serialization import contract_fingerprint
44
+ from equiframe.tables import (
45
+ ArrowInput,
46
+ ArtifactInput,
47
+ LoadedTable,
48
+ capture_artifact,
49
+ detached_arrow,
50
+ require_dependency,
51
+ )
52
+ from equiframe.types import ArtifactRef
53
+
54
+ from .ports import CompiledPlan, CompilerRegistry, native_compiler_registry
55
+ from .runner_types import (
56
+ CompilerRecord,
57
+ MigrationResult,
58
+ MigrationSpec,
59
+ MigrationStage,
60
+ MigrationStageName,
61
+ pipeline_complete,
62
+ )
63
+ from .types import CompilationCheck
64
+ from .validation import ir_fingerprint
65
+
66
+ UTC = _timezone.utc
67
+ _T = TypeVar("_T")
68
+
69
+ if TYPE_CHECKING:
70
+ from equiframe.contracts import TableSpec
71
+
72
+ ExpectedInput = TypeAliasType("ExpectedInput", ArrowInput | ArtifactRef | Path)
73
+
74
+
75
+ def check_migration(
76
+ spec: MigrationSpec,
77
+ *,
78
+ compilers: CompilerRegistry | None = None,
79
+ ) -> tuple[CompilationCheck, CompilationCheck]:
80
+ """Check both compiler capabilities without loading inputs or native engines."""
81
+ spec = MigrationSpec.model_validate(spec)
82
+ registry = compilers if compilers is not None else native_compiler_registry()
83
+ return (
84
+ registry.resolve(spec.source_engine).check(spec.ir, spec.contract),
85
+ registry.resolve(spec.target_engine).check(spec.ir, spec.contract),
86
+ )
87
+
88
+
89
+ class _Steps:
90
+ def __init__(self, journal: EventJournal, token: CancellationToken, deadline: float | None):
91
+ self.journal = journal
92
+ self.token = token
93
+ self.deadline = deadline
94
+ self.records: list[MigrationStage] = []
95
+
96
+ def check(self) -> None:
97
+ if self.token.cancelled:
98
+ raise ExecutionCancelled("migration cancelled at a cooperative checkpoint")
99
+ if self.deadline is not None and time.perf_counter() >= self.deadline:
100
+ raise ExecutionTimeout("migration budget exceeded at a cooperative checkpoint")
101
+
102
+ def run(
103
+ self,
104
+ name: MigrationStageName,
105
+ action: Callable[[], _T],
106
+ complete: Callable[[_T], bool],
107
+ *,
108
+ skip: str | None = None,
109
+ ) -> _T | None:
110
+ if skip:
111
+ record = MigrationStage(stage=name, state="skipped", code=skip, elapsed_seconds=0.0)
112
+ result = None
113
+ else:
114
+ start = time.perf_counter()
115
+ self.journal.emit(name, "started", "stage_started")
116
+ result = None
117
+ try:
118
+ self.check()
119
+ result = action()
120
+ self.check()
121
+ ok = complete(result)
122
+ record = MigrationStage(
123
+ stage=name,
124
+ state="completed" if ok else "not_evaluable",
125
+ code="stage_completed" if ok else "component_unavailable",
126
+ elapsed_seconds=time.perf_counter() - start,
127
+ )
128
+ except Exception as error:
129
+ result = None
130
+ code = (
131
+ "missing_dependency"
132
+ if isinstance(error, MissingDependencyError)
133
+ else "unsupported_capability"
134
+ if isinstance(error, CapabilityError)
135
+ else "cancelled"
136
+ if isinstance(error, ExecutionCancelled)
137
+ else "timeout"
138
+ if isinstance(error, ExecutionTimeout)
139
+ else "resource_exhausted"
140
+ if isinstance(error, (ResourceLimitError, MemoryError))
141
+ else "invalid_input"
142
+ if isinstance(error, (InvalidExecutionInput, ValueError))
143
+ else "operation_failed"
144
+ )
145
+ record = MigrationStage(
146
+ stage=name,
147
+ state="failed",
148
+ code=code,
149
+ elapsed_seconds=time.perf_counter() - start,
150
+ )
151
+ self.records.append(record)
152
+ self.journal.emit(name, record.state, record.code)
153
+ return result
154
+
155
+ def timeout(self, declared: float | None) -> float | None:
156
+ self.check()
157
+ remaining = self.deadline - time.perf_counter() if self.deadline is not None else None
158
+ if remaining is not None and remaining <= 0:
159
+ raise ExecutionTimeout("migration budget exhausted")
160
+ return (
161
+ min(declared, remaining)
162
+ if declared is not None and remaining is not None
163
+ else (declared if remaining is None else remaining)
164
+ )
165
+
166
+
167
+ def _validate_input(loaded: LoadedTable, declared: TableSpec, context: RunContext) -> None:
168
+ """Check actual data; normalized values are inspected but never written back."""
169
+ context.check_table(loaded.table, output=False)
170
+ columns = declared.schema_spec.columns
171
+ if tuple(loaded.table.column_names) != tuple(c.name for c in columns):
172
+ raise InvalidExecutionInput("captured input columns/order differ from the contract")
173
+ for column in columns:
174
+ values = loaded.table[column.name]
175
+ if logical_signature(values.type) != tuple(column.logical_type.model_dump().values()):
176
+ raise InvalidExecutionInput("captured input physical type differs from the contract")
177
+ for chunk in values.chunks:
178
+ context.check()
179
+ for n, scalar in enumerate(chunk):
180
+ if n % 4096 == 0:
181
+ context.check()
182
+ _, violations = normalize(physical_value(scalar), column)
183
+ if violations:
184
+ raise InvalidExecutionInput(
185
+ "captured input violates declared value constraints"
186
+ )
187
+
188
+
189
+ class _PinnedSource:
190
+ def __init__(self, inputs: tuple[TableArtifact, ...]) -> None:
191
+ self._inputs = {t.name: t for t in inputs}
192
+
193
+ @property
194
+ def table_names(self) -> tuple[str, ...]:
195
+ return tuple(self._inputs)
196
+
197
+ def resolve(self, name: str, *, context: RunContext) -> LoadedTable:
198
+ ref = self._inputs[name]
199
+ value = ArtifactInput(ref.data).load(context)
200
+ index = ArtifactInput(ref.index).load(context).table if ref.index else None
201
+ return LoadedTable(
202
+ value.table,
203
+ native_types=tuple((column.name, column.engine_type) for column in ref.columns),
204
+ index=index,
205
+ origin=ref.data,
206
+ notes=(
207
+ *ref.notes,
208
+ "Fixed migration snapshot; original provider is not called again.",
209
+ ),
210
+ )
211
+
212
+
213
+ def _compiler_record(compiled: CompiledPlan, spec: MigrationSpec) -> CompilerRecord:
214
+ t = compiled.transformation
215
+ if (
216
+ compiled.engine != t.engine
217
+ or not compiled.check.supported
218
+ or compiled.check.engine != compiled.engine
219
+ or compiled.ir_sha256 != ir_fingerprint(spec.ir)
220
+ or compiled.contract_sha256 != contract_fingerprint(spec.contract)
221
+ ):
222
+ raise InvalidExecutionInput("compiler output contradicts its specification")
223
+ return CompilerRecord(
224
+ engine=t.engine,
225
+ transformation_id=t.transformation_id,
226
+ revision=t.revision,
227
+ code_version=t.code_version,
228
+ ir_sha256=compiled.ir_sha256,
229
+ contract_sha256=compiled.contract_sha256,
230
+ check=compiled.check,
231
+ )
232
+
233
+
234
+ def migrate(
235
+ spec: MigrationSpec,
236
+ *,
237
+ source: DataSource,
238
+ work_dir: Path | None = None,
239
+ expected: ExpectedInput | None = None,
240
+ compilers: CompilerRegistry | None = None,
241
+ adapters: Mapping[str, EngineAdapter] | None = None,
242
+ rule_registry: RuleRegistry | None = None,
243
+ detector_bundle: DetectorBundle | None = None,
244
+ cancellation: CancellationToken | None = None,
245
+ ) -> MigrationResult:
246
+ """Execute native origin/destination; never translate arbitrary Python or execute JSON code.
247
+
248
+ Invalid configuration and publication failures raise. Operational stage failures
249
+ persist a non-success result. Captures and callbacks are synchronous; budgets
250
+ are cooperative checkpoints, not process memory quotas or preemptive threads.
251
+ """
252
+ spec = MigrationSpec.model_validate(spec)
253
+ if detector_bundle is not None:
254
+ detector_bundle = DetectorBundle.model_validate(detector_bundle)
255
+ if spec.profile is None:
256
+ raise ValueError("a detector bundle requires an explicit profile")
257
+ identifier = "migration_" + uuid4().hex
258
+ directory = (
259
+ local_output_directory(Path.cwd() / "equiframe-runs" if work_dir is None else work_dir)
260
+ / identifier
261
+ )
262
+ directory.mkdir(parents=True, exist_ok=False)
263
+ token = cancellation if cancellation is not None else CancellationToken()
264
+ start = time.perf_counter()
265
+ started = datetime.now(UTC).isoformat()
266
+ deadline = start + spec.options.timeout_seconds if spec.options.timeout_seconds else None
267
+ journal = EventJournal(directory, identifier)
268
+ journal.emit("migration", "started", "migration_started")
269
+ publish_json(spec, directory / "spec.json")
270
+ if detector_bundle is not None:
271
+ publish_json(detector_bundle, directory / "detector-bundle.json")
272
+ steps = _Steps(journal, token, deadline)
273
+ compiler_registry = compilers if compilers is not None else native_compiler_registry()
274
+ resolved_adapters: dict[str, EngineAdapter] = {}
275
+
276
+ def preflight() -> tuple[CompiledPlan, CompiledPlan]:
277
+ engines = (spec.source_engine, spec.target_engine)
278
+ checks = check_migration(spec, compilers=compiler_registry)
279
+ if any(not c.supported for c in checks):
280
+ raise CapabilityError("migration IR is unsupported by a configured compiler")
281
+ compiled = tuple(
282
+ compiler_registry.resolve(e).compile(spec.ir, spec.contract) for e in engines
283
+ )
284
+ records = tuple(_compiler_record(c, spec) for c in compiled)
285
+ if tuple(r.engine for r in records) != engines:
286
+ raise InvalidExecutionInput("compiled engines do not match the pair")
287
+ if len(set(source.table_names)) != len(source.table_names) or set(source.table_names) != {
288
+ t.name for t in spec.contract.inputs
289
+ }:
290
+ raise InvalidExecutionInput("provider bindings do not match the contract")
291
+ for engine, record, implementation in zip(engines, records, compiled, strict=True):
292
+ adapter = (
293
+ adapters[engine] if adapters and engine in adapters else native_adapter(engine)
294
+ )
295
+ capabilities = adapter.capabilities()
296
+ if adapter.engine != engine or capabilities.engine != engine:
297
+ raise InvalidExecutionInput("adapter does not match its configured engine")
298
+ for dependency in capabilities.required_packages:
299
+ require_dependency(dependency, engine)
300
+ _contract_capabilities(
301
+ ExecutionPlan(
302
+ contract=spec.contract,
303
+ engine=engine,
304
+ transformation_id=record.transformation_id,
305
+ transformation_revision=record.revision,
306
+ options=spec.execution_options,
307
+ ),
308
+ capabilities,
309
+ timestamp_codec=_timestamp_codec(adapter, implementation.transformation),
310
+ )
311
+ resolved_adapters[engine] = adapter
312
+ steps.check()
313
+ return compiled[0], compiled[1]
314
+
315
+ compiled = steps.run("preflight", preflight, lambda _: True)
316
+ records = tuple(_compiler_record(c, spec) for c in compiled) if compiled else ()
317
+ captured: list[TableArtifact] = []
318
+ expected_ref: ArtifactRef | None = None
319
+
320
+ def capture() -> bool:
321
+ nonlocal expected_ref
322
+ if not (compiled is not None):
323
+ raise AssertionError("Internal invariant check failed.")
324
+ capture_dir = directory / "capture"
325
+ capture_dir.mkdir(exist_ok=False)
326
+ context = RunContext(
327
+ identifier,
328
+ identifier + ".capture",
329
+ ExecutionPlan(
330
+ contract=spec.contract,
331
+ engine=spec.source_engine,
332
+ transformation_id=records[0].transformation_id,
333
+ options=spec.execution_options,
334
+ ),
335
+ capture_dir,
336
+ LocalArtifactStore(capture_dir / "artifacts"),
337
+ token,
338
+ deadline,
339
+ )
340
+ rows = total_bytes = 0
341
+ for table in spec.contract.inputs:
342
+ context.check()
343
+ value = source.resolve(table.name, context=context)
344
+ if not isinstance(value, LoadedTable):
345
+ raise InvalidExecutionInput("provider must return LoadedTable")
346
+ context.check_table(value.table, output=False)
347
+ if value.meaningful_index or (
348
+ value.index is not None and spec.comparison_policy.index != "ignore"
349
+ ):
350
+ raise CapabilityError(
351
+ "input index must be explicit columns; RangeIndex needs authorization"
352
+ )
353
+ value = LoadedTable(
354
+ detached_arrow(value.table),
355
+ native_types=value.native_types,
356
+ index=detached_arrow(value.index) if value.index is not None else None,
357
+ origin=value.origin,
358
+ notes=value.notes,
359
+ )
360
+ _validate_input(value, table, context)
361
+ for implementation in compiled:
362
+ engine = implementation.engine
363
+ if engine in ("pandas", "polars", "duckdb"):
364
+ codec = _timestamp_codec(
365
+ resolved_adapters[engine], implementation.transformation
366
+ )
367
+ native_value = codec.encode_input(table.name, value) if codec else value
368
+ _input_capabilities(native_value, engine, context)
369
+ if engine == "duckdb":
370
+ duckdb_temporal_capabilities(native_value.table)
371
+ rows += value.table.num_rows
372
+ total_bytes += value.table.nbytes
373
+ if value.index is not None:
374
+ context.check_table(value.index, output=False)
375
+ total_bytes += value.index.nbytes
376
+ if rows > spec.options.max_capture_rows or total_bytes > spec.options.max_capture_bytes:
377
+ raise ResourceLimitError("aggregate migration capture budget exhausted")
378
+ captured.append(capture_artifact(table.name, value, context))
379
+ if expected is not None:
380
+ expected_dir = capture_dir / "expected"
381
+ expected_dir.mkdir(exist_ok=False)
382
+ ref = snapshot(expected, expected_dir, spec.comparison_options, steps.check)
383
+ expected_rows, expected_bytes = physical_size(ref, check=steps.check)
384
+ rows += expected_rows
385
+ total_bytes += expected_bytes
386
+ if rows > spec.options.max_capture_rows or total_bytes > spec.options.max_capture_bytes:
387
+ raise ResourceLimitError("aggregate capture including expected exceeds budget")
388
+ expected_ref = ref
389
+ publish_json(
390
+ {
391
+ "schema_version": "1.0",
392
+ "inputs": [t.model_dump(mode="json") for t in captured],
393
+ "expected": expected_ref.model_dump(mode="json") if expected_ref else None,
394
+ "rows": rows,
395
+ "arrow_buffer_bytes": total_bytes,
396
+ },
397
+ directory / "capture.json",
398
+ )
399
+ return True
400
+
401
+ ready = steps.run(
402
+ "capture",
403
+ capture,
404
+ bool,
405
+ skip="preflight_unavailable" if compiled is None else None,
406
+ )
407
+ pinned = _PinnedSource(tuple(captured))
408
+ registry = TransformationRegistry()
409
+ if compiled:
410
+ for c in compiled:
411
+ registry.register(c.transformation)
412
+
413
+ def native(n: int) -> PipelineResult:
414
+ record = records[n]
415
+ options = spec.execution_options.model_copy(
416
+ update={
417
+ "timeout_seconds": steps.timeout(spec.execution_options.timeout_seconds),
418
+ }
419
+ )
420
+ return run_pipeline(
421
+ PipelineSpec(
422
+ execution=ExecutionPlan(
423
+ contract=spec.contract,
424
+ engine=record.engine,
425
+ transformation_id=record.transformation_id,
426
+ transformation_revision=record.revision,
427
+ options=options,
428
+ ),
429
+ profile=spec.profile,
430
+ profile_options=spec.profile_options,
431
+ rule_options=spec.rule_options,
432
+ comparison_policy=spec.comparison_policy,
433
+ comparison_options=spec.comparison_options,
434
+ ),
435
+ source=pinned,
436
+ registry=registry,
437
+ work_dir=directory,
438
+ adapter=resolved_adapters[record.engine],
439
+ rule_registry=rule_registry,
440
+ detector_bundle=detector_bundle,
441
+ cancellation=token,
442
+ )
443
+
444
+ src = steps.run(
445
+ "source",
446
+ lambda: native(0),
447
+ lambda p: pipeline_complete(spec, p),
448
+ skip="capture_unavailable" if ready is None else None,
449
+ )
450
+ dst = steps.run(
451
+ "target",
452
+ lambda: native(1),
453
+ lambda p: pipeline_complete(spec, p),
454
+ skip="capture_unavailable" if ready is None else None,
455
+ )
456
+
457
+ def compared(
458
+ left: PipelineResult | None,
459
+ right: PipelineResult | ArtifactRef | None,
460
+ reference: Literal["peer", "expected"],
461
+ ) -> ComparisonResult:
462
+ if not (left is not None and left.execution is not None and right is not None):
463
+ raise AssertionError("Internal invariant check failed.")
464
+ target = right.execution if isinstance(right, PipelineResult) else right
465
+ if not (target is not None):
466
+ raise AssertionError("Internal invariant check failed.")
467
+ return compare(
468
+ left.execution,
469
+ target,
470
+ contract=spec.contract,
471
+ policy=spec.comparison_policy,
472
+ options=spec.comparison_options.model_copy(
473
+ update={
474
+ "timeout_seconds": steps.timeout(spec.comparison_options.timeout_seconds),
475
+ }
476
+ ),
477
+ reference=reference,
478
+ work_dir=directory,
479
+ cancellation=token,
480
+ )
481
+
482
+ def has_output(p: PipelineResult | None) -> bool:
483
+ return p is not None and p.execution is not None and p.execution.state == "completed"
484
+
485
+ peer = steps.run(
486
+ "comparison",
487
+ lambda: compared(src, dst, "peer"),
488
+ lambda c: c.complete and c.state != "not_evaluable",
489
+ skip=None if has_output(src) and has_output(dst) else "execution_unavailable",
490
+ )
491
+ verifications: list[ComparisonResult | None] = []
492
+ verification_steps: tuple[tuple[MigrationStageName, PipelineResult | None], ...] = (
493
+ ("verification_source", src),
494
+ ("verification_target", dst),
495
+ )
496
+ for name, child in verification_steps:
497
+ verifications.append(
498
+ steps.run(
499
+ name,
500
+ partial(compared, child, expected_ref, "expected"),
501
+ lambda c: c.complete and c.state != "not_evaluable",
502
+ skip="not_requested"
503
+ if expected is None
504
+ else "expected_unavailable"
505
+ if expected_ref is None
506
+ else "execution_unavailable"
507
+ if not has_output(child)
508
+ else None,
509
+ )
510
+ )
511
+ journal.emit("migration", "finished", "migration_finished")
512
+ result = MigrationResult(
513
+ migration_id=identifier,
514
+ spec=spec,
515
+ spec_sha256=profile_digest(spec.model_dump(mode="json")),
516
+ started_at=started,
517
+ finished_at=datetime.now(UTC).isoformat(),
518
+ elapsed_seconds=time.perf_counter() - start,
519
+ stages=tuple(steps.records),
520
+ compilers=records,
521
+ inputs=tuple(captured),
522
+ expected_requested=expected is not None,
523
+ expected=expected_ref,
524
+ detection_requested=detector_bundle is not None,
525
+ source=src,
526
+ target=dst,
527
+ comparison=peer,
528
+ verification_source=verifications[0],
529
+ verification_target=verifications[1],
530
+ cancelled=token.cancelled,
531
+ evidence=(),
532
+ event_count=journal.count,
533
+ event_head_sha256=journal.head,
534
+ )
535
+ from .io import migration_summary, render_migration_report
536
+
537
+ publish_json(migration_summary(result), directory / "report.json")
538
+ from equiframe._document_io import write_document as _write_document
539
+
540
+ _write_document(directory / "report.html", render_migration_report(result).encode("utf-8"))
541
+ result = MigrationResult.model_validate(
542
+ {**result.model_dump(), "evidence": inventory(directory)}
543
+ )
544
+ publish_json(result, directory / "migration.json")
545
+ return result