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,231 @@
1
+ """Exact native Decimal coefficient arithmetic with explicit overflow checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from equiframe.errors import CapabilityError
8
+
9
+ if TYPE_CHECKING:
10
+ import polars as pl
11
+
12
+ from equiframe.execution import RunContext
13
+
14
+
15
+ def divide(
16
+ frame: pl.DataFrame,
17
+ left: pl.Expr,
18
+ right: pl.Expr,
19
+ *,
20
+ left_scale: int,
21
+ right_scale: int,
22
+ precision: int,
23
+ scale: int,
24
+ rounding: str,
25
+ prefix: str,
26
+ context: RunContext,
27
+ ) -> tuple[pl.DataFrame, pl.Expr]:
28
+ """Materialize native quotient/remainder steps; never use floats or row callbacks.
29
+
30
+ Each operand coefficient fits 38 decimal digits. Modular doubling computes
31
+ the next digit without multiplying a potentially 38-digit remainder by ten.
32
+ Decimal(38,0) carries exact coefficient arithmetic: Polars 1.30 panics on
33
+ Int128 column/column addition and subtraction. Integer floor/modulo remain
34
+ native Int128 operations; every resulting coefficient still fits 38 digits.
35
+ Only finite native boolean reductions cross back to the coordinator to reject
36
+ invalid arithmetic; neither operands nor answers are computed in Python.
37
+ """
38
+ import polars as pl
39
+
40
+ def col(name: str) -> pl.Expr:
41
+ return pl.col(prefix + name)
42
+
43
+ def named(value: pl.Expr, name: str) -> pl.Expr:
44
+ return value.alias(prefix + name)
45
+
46
+ wide = pl.Decimal(38, 0)
47
+ zero = pl.lit(0, dtype=wide)
48
+
49
+ def constant(value: int) -> pl.Expr:
50
+ return pl.lit(str(value)).cast(wide)
51
+
52
+ def floor(left: pl.Expr, right: pl.Expr) -> pl.Expr:
53
+ # The native direct Int128 -> Decimal cast rejects valid large values
54
+ # in Polars 1.30. Its native integer text and Decimal parsers are exact.
55
+ return (left.cast(pl.Int128) // right.cast(pl.Int128)).cast(pl.String).cast(wide)
56
+
57
+ def modulo(left: pl.Expr, right: pl.Expr) -> pl.Expr:
58
+ return (left.cast(pl.Int128) % right.cast(pl.Int128)).cast(pl.String).cast(wide)
59
+
60
+ frame = frame.with_columns(
61
+ named(left.is_not_null() & right.is_not_null(), "active"),
62
+ named(left.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "a"),
63
+ named(right.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "b"),
64
+ )
65
+
66
+ def reject(condition: pl.Expr, reason: str) -> None:
67
+ context.check()
68
+ if frame.select((col("active") & condition).any()).item():
69
+ raise CapabilityError("Polars Decimal division: " + reason)
70
+
71
+ reject(col("b") == 0, "zero denominator")
72
+ frame = frame.with_columns(
73
+ named((col("a") < 0).xor(col("b") < 0), "negative"),
74
+ named(pl.when(col("active")).then(col("a").abs()).otherwise(zero), "a"),
75
+ named(
76
+ pl.when(col("active")).then(col("b").abs()).otherwise(constant(1)),
77
+ "b",
78
+ ),
79
+ )
80
+ frame = frame.with_columns(
81
+ named(floor(col("a"), col("b")), "q"), named(modulo(col("a"), col("b")), "r")
82
+ )
83
+ exponent = scale + right_scale - left_scale
84
+ limit = 10**precision - 1
85
+ if exponent >= 0:
86
+ reject(col("q") > constant(limit), "output precision exceeded")
87
+ for _ in range(exponent):
88
+ reject(col("q") > constant(limit // 10), "output precision exceeded")
89
+ remainder, denominator = col("r"), col("b")
90
+ carry = remainder >= denominator - remainder
91
+ frame = frame.with_columns(
92
+ named(carry.cast(pl.Int64).cast(wide), "c2"),
93
+ named(
94
+ remainder
95
+ - pl.when(carry).then(denominator - remainder).otherwise(zero - remainder),
96
+ "r2",
97
+ ),
98
+ )
99
+ for first, second in ((2, 4), (4, 8)):
100
+ remainder = col("r" + str(first))
101
+ carry = remainder >= denominator - remainder
102
+ frame = frame.with_columns(
103
+ named(
104
+ col("c" + str(first)) * constant(2) + carry.cast(pl.Int64).cast(wide),
105
+ "c" + str(second),
106
+ ),
107
+ named(
108
+ remainder
109
+ - pl.when(carry).then(denominator - remainder).otherwise(zero - remainder),
110
+ "r" + str(second),
111
+ ),
112
+ )
113
+ r8, r2 = col("r8"), col("r2")
114
+ carry = r8 >= denominator - r2
115
+ frame = frame.with_columns(
116
+ named(
117
+ col("q") * constant(10)
118
+ + col("c8")
119
+ + col("c2")
120
+ + carry.cast(pl.Int64).cast(wide),
121
+ "q",
122
+ ),
123
+ named(r8 - pl.when(carry).then(denominator - r2).otherwise(zero - r2), "r"),
124
+ )
125
+ remainder = col("r")
126
+ exact = remainder == 0
127
+ above = remainder > col("b") - remainder
128
+ tie = remainder == col("b") - remainder
129
+ else:
130
+ factor = pl.lit(10 ** (-exponent), dtype=pl.Int128)
131
+ frame = frame.with_columns(
132
+ named(modulo(col("q"), factor), "dropped"), named(floor(col("q"), factor), "q")
133
+ )
134
+ dropped, remainder = col("dropped"), col("r")
135
+ half = constant(5 * 10 ** (-exponent - 1))
136
+ exact = (dropped == 0) & (remainder == 0)
137
+ above = (dropped > half) | ((dropped == half) & (remainder > 0))
138
+ tie = (dropped == half) & (remainder == 0)
139
+ if rounding == "exact":
140
+ reject(~exact, "declared exact scale is insufficient")
141
+ increment = pl.lit(False)
142
+ elif rounding == "half_even":
143
+ increment = above | (tie & (modulo(col("q"), constant(2)) == 1))
144
+ elif rounding == "half_away_from_zero":
145
+ increment = above | tie
146
+ else:
147
+ raise CapabilityError("unsupported Decimal division rounding")
148
+ reject(
149
+ (col("q") > constant(limit)) | ((col("q") == constant(limit)) & increment),
150
+ "output precision exceeded",
151
+ )
152
+ frame = frame.with_columns(named(col("q") + increment.cast(pl.Int64).cast(wide), "q"))
153
+ text = col("q").cast(pl.String)
154
+ if scale:
155
+ text = text.str.pad_start(scale + 1, "0")
156
+ text = pl.concat_str(
157
+ text.str.slice(0, text.str.len_chars() - scale), pl.lit("."), text.str.slice(-scale)
158
+ )
159
+ text = pl.concat_str(
160
+ pl.when(col("negative") & (col("q") != 0)).then(pl.lit("-")).otherwise(pl.lit("")),
161
+ text,
162
+ )
163
+ output = prefix + "result"
164
+ frame = frame.with_columns(
165
+ pl.when(col("active"))
166
+ .then(text.cast(pl.Decimal(precision, scale), strict=True))
167
+ .otherwise(None)
168
+ .alias(output)
169
+ )
170
+ return frame, pl.col(output)
171
+
172
+
173
+ def multiply(
174
+ frame: pl.DataFrame,
175
+ left: pl.Expr,
176
+ right: pl.Expr,
177
+ *,
178
+ precision: int,
179
+ scale: int,
180
+ prefix: str,
181
+ context: RunContext,
182
+ ) -> tuple[pl.DataFrame, pl.Expr]:
183
+ """Bound native coefficient multiplication before the product can overflow."""
184
+ import polars as pl
185
+
186
+ def col(name: str) -> pl.Expr:
187
+ return pl.col(prefix + name)
188
+
189
+ def named(value: pl.Expr, name: str) -> pl.Expr:
190
+ return value.alias(prefix + name)
191
+
192
+ wide = pl.Decimal(38, 0)
193
+ zero, one = pl.lit(0, dtype=wide), pl.lit(1, dtype=wide)
194
+ limit = pl.lit(str(10**precision - 1)).cast(wide)
195
+ context.check()
196
+ frame = frame.with_columns(
197
+ named(left.is_not_null() & right.is_not_null(), "active"),
198
+ named(left.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "a"),
199
+ named(right.cast(pl.String).str.replace_all(r"\.", "").cast(wide), "b"),
200
+ ).with_columns(
201
+ named((col("a") < 0).xor(col("b") < 0), "negative"),
202
+ named(pl.when(col("active")).then(col("a").abs()).otherwise(zero), "a"),
203
+ named(pl.when(col("active")).then(col("b").abs()).otherwise(zero), "b"),
204
+ )
205
+ denominator = pl.when(col("b") != 0).then(col("b")).otherwise(one)
206
+ bound = (limit.cast(pl.Int128) // denominator.cast(pl.Int128)).cast(pl.String).cast(wide)
207
+ too_large = col("active") & (col("b") != 0) & (col("a") > bound)
208
+ if frame.select(too_large.any()).item():
209
+ raise CapabilityError("Polars Decimal multiply: output precision exceeded")
210
+ context.check()
211
+ # Both magnitudes fit the declared coefficient bound before this product.
212
+ # No intermediate is scaled up to the final scale or widened through float.
213
+ frame = frame.with_columns(named(col("a") * col("b"), "value"))
214
+ text = col("value").cast(pl.String)
215
+ if scale:
216
+ text = text.str.pad_start(scale + 1, "0")
217
+ text = pl.concat_str(
218
+ text.str.slice(0, text.str.len_chars() - scale), pl.lit("."), text.str.slice(-scale)
219
+ )
220
+ text = pl.concat_str(
221
+ pl.when(col("negative") & (col("value") != 0)).then(pl.lit("-")).otherwise(pl.lit("")),
222
+ text,
223
+ )
224
+ output = prefix + "result"
225
+ frame = frame.with_columns(
226
+ pl.when(col("active"))
227
+ .then(text.cast(pl.Decimal(precision, scale), strict=True))
228
+ .otherwise(None)
229
+ .alias(output)
230
+ )
231
+ return frame, pl.col(output)
@@ -0,0 +1,198 @@
1
+ """Read and verify migration evidence without executing ETL, compilers or plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from equiframe._physical import physical_size, verify_table_metadata
8
+ from equiframe._report_format_v1 import (
9
+ migration_html_v1,
10
+ migration_legacy_summary_v1,
11
+ migration_summary_v1,
12
+ )
13
+ from equiframe.comparison import iter_differences, read_comparison
14
+ from equiframe.errors import ArtifactIntegrityError
15
+ from equiframe.evidence import (
16
+ describe_file,
17
+ load_json,
18
+ parse_json,
19
+ read_pipeline,
20
+ verify_events,
21
+ verify_files,
22
+ )
23
+ from equiframe.local import local_path
24
+ from equiframe.types import ArtifactRef
25
+
26
+ from .runner_types import MigrationResult, MigrationSpec
27
+
28
+
29
+ def _legacy_summary(result: MigrationResult) -> dict[str, object]:
30
+ return migration_legacy_summary_v1(result)
31
+
32
+
33
+ def migration_summary(result: MigrationResult) -> dict[str, object]:
34
+ return migration_summary_v1(result)
35
+
36
+
37
+ def _render_report(result: MigrationResult, *, legacy: bool = False) -> str:
38
+ return migration_html_v1(result, legacy=legacy)
39
+
40
+
41
+ def render_migration_report(result: MigrationResult) -> str:
42
+ return _render_report(result)
43
+
44
+
45
+ def read_migration(path: Path, *, verify: bool = True) -> MigrationResult:
46
+ """Check a closed bundle; explicit False reads metadata without byte verification."""
47
+ if type(verify) is not bool:
48
+ raise TypeError("verify must be a bool; only explicit False skips integrity checks")
49
+ path = path / "migration.json" if path.is_dir() else path
50
+ result = load_json(MigrationResult, path)
51
+ if not verify:
52
+ return result
53
+ directory = path.parent.resolve()
54
+ listed = {i.path: i for i in result.evidence}
55
+ verify_files(directory, result.evidence)
56
+
57
+ def bind(ref: ArtifactRef) -> None:
58
+ ref_path = local_path(ref).resolve()
59
+ if not ref_path.is_relative_to(directory):
60
+ raise ArtifactIntegrityError("migration artifact escapes its operation")
61
+ item = listed.get(ref_path.relative_to(directory).as_posix())
62
+ if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
63
+ raise ArtifactIntegrityError("migration artifact contradicts its inventory")
64
+
65
+ actual = {
66
+ p.relative_to(directory).as_posix()
67
+ for p in directory.rglob("*")
68
+ if p.is_file() and p != path.resolve()
69
+ }
70
+ if actual != set(listed):
71
+ raise ArtifactIntegrityError("migration files contradict its closed inventory")
72
+ for name in ("events.jsonl", "spec.json", "report.json", "report.html"):
73
+ if name not in listed:
74
+ raise ArtifactIntegrityError("required migration evidence is missing")
75
+ if load_json(MigrationSpec, directory / "spec.json") != result.spec:
76
+ raise ArtifactIntegrityError("migration configuration contradicts the manifest")
77
+ events = verify_events(directory / "events.jsonl", operation_id=result.migration_id)
78
+ if (
79
+ not events
80
+ or len(events) != result.event_count
81
+ or events[-1].sha256 != result.event_head_sha256
82
+ or (events[0].stage, events[0].state) != ("migration", "started")
83
+ or (events[-1].stage, events[-1].state) != ("migration", "finished")
84
+ ):
85
+ raise ArtifactIntegrityError("migration journal contradicts its manifest")
86
+ terminal = tuple(
87
+ (e.stage, e.state, e.code)
88
+ for e in events
89
+ if e.stage != "migration" and e.state != "started"
90
+ )
91
+ if terminal != tuple((s.stage, s.state, s.code) for s in result.stages):
92
+ raise ArtifactIntegrityError("migration stage outcomes contradict its journal")
93
+ refs = []
94
+ for table in result.inputs:
95
+ bind(table.data)
96
+ if table.index:
97
+ bind(table.index)
98
+ verify_table_metadata(table)
99
+ refs.append(table.data)
100
+ if table.index:
101
+ refs.append(table.index)
102
+ if result.expected:
103
+ bind(result.expected)
104
+ refs.append(result.expected)
105
+ if result.stages[1].state == "completed":
106
+ if "capture.json" not in listed:
107
+ raise ArtifactIntegrityError("complete capture receipt is missing")
108
+ captured = parse_json((directory / "capture.json").read_bytes())
109
+ if (
110
+ not isinstance(captured, dict)
111
+ or set(captured)
112
+ != {
113
+ "schema_version",
114
+ "inputs",
115
+ "expected",
116
+ "rows",
117
+ "arrow_buffer_bytes",
118
+ }
119
+ or (
120
+ captured.get("schema_version") != "1.0"
121
+ or captured.get("inputs") != [t.model_dump(mode="json") for t in result.inputs]
122
+ or captured.get("expected")
123
+ != (result.expected.model_dump(mode="json") if result.expected else None)
124
+ )
125
+ ):
126
+ raise ArtifactIntegrityError("capture receipt contradicts fixed inputs")
127
+ sizes = [physical_size(t.data) for t in result.inputs]
128
+ expected_size = physical_size(result.expected) if result.expected else (0, 0)
129
+ observed_rows = sum(n for n, _ in sizes) + expected_size[0]
130
+ observed_bytes = sum(n for _, n in sizes) + expected_size[1]
131
+ observed_bytes += sum(physical_size(t.index)[1] for t in result.inputs if t.index)
132
+ if (
133
+ type(captured.get("rows")) is not int
134
+ or type(captured.get("arrow_buffer_bytes")) is not int
135
+ or captured.get("rows") != observed_rows
136
+ or captured.get("arrow_buffer_bytes") != observed_bytes
137
+ or observed_rows > result.spec.options.max_capture_rows
138
+ or observed_bytes > result.spec.options.max_capture_bytes
139
+ ):
140
+ raise ArtifactIntegrityError("capture physical statistics contradict the receipt")
141
+ for p in (result.source, result.target):
142
+ if p is not None and read_pipeline(directory / p.pipeline_id) != p:
143
+ raise ArtifactIntegrityError("native pipeline contradicts migration manifest")
144
+ if p is not None and p.execution is not None:
145
+ for t in (*p.execution.inputs, *((p.execution.output,) if p.execution.output else ())):
146
+ bind(t.data)
147
+ if t.index:
148
+ bind(t.index)
149
+ verify_table_metadata(t)
150
+ for c in (result.comparison, result.verification_source, result.verification_target):
151
+ if c is None:
152
+ continue
153
+ if read_comparison(directory / c.comparison_id / "comparison.json") != c:
154
+ raise ArtifactIntegrityError("comparison contradicts migration manifest")
155
+ bind(c.differences)
156
+ count = violations = 0
157
+ for difference in iter_differences(c.differences):
158
+ if count < len(c.examples) and difference != c.examples[count]:
159
+ raise ArtifactIntegrityError("comparison examples contradict complete differences")
160
+ count += 1
161
+ violations += int(difference.category in ("contract", "schema"))
162
+ if count != c.counts.difference_records or violations != c.counts.contract_violations:
163
+ raise ArtifactIntegrityError("comparison differences contradict recorded counts")
164
+ if len(c.examples) != min(count, c.options.max_examples):
165
+ raise ArtifactIntegrityError("comparison example count contradicts configured budget")
166
+ refs.append(c.differences)
167
+ refs.extend(s.data for s in (c.left, c.right) if s is not None)
168
+ for ref in refs:
169
+ ref_path = local_path(ref).resolve()
170
+ if not ref_path.is_relative_to(directory):
171
+ raise ArtifactIntegrityError("migration artifact escapes its operation")
172
+ item = listed.get(ref_path.relative_to(directory).as_posix())
173
+ if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
174
+ raise ArtifactIntegrityError("migration artifact contradicts its inventory")
175
+ if describe_file(ref_path, directory) != item:
176
+ raise ArtifactIntegrityError("migration artifact changed")
177
+ stored_summary = parse_json((directory / "report.json").read_bytes())
178
+ legacy = stored_summary == migration_legacy_summary_v1(result)
179
+ if not legacy and stored_summary != migration_summary_v1(result):
180
+ raise ArtifactIntegrityError("migration report contradicts its manifest")
181
+ if (directory / "report.html").read_bytes() != migration_html_v1(result, legacy=legacy).encode(
182
+ "utf-8"
183
+ ):
184
+ raise ArtifactIntegrityError("migration HTML contradicts its manifest")
185
+ if result.detection_requested:
186
+ from equiframe.detector_types import DetectorBundle
187
+
188
+ if "detector-bundle.json" not in listed:
189
+ raise ArtifactIntegrityError("migration detector bundle is missing")
190
+ bundle = load_json(DetectorBundle, directory / "detector-bundle.json")
191
+ for p in (result.source, result.target):
192
+ if (
193
+ p is not None
194
+ and load_json(DetectorBundle, directory / p.pipeline_id / "detector-bundle.json")
195
+ != bundle
196
+ ):
197
+ raise ArtifactIntegrityError("native pipelines used different detector bundles")
198
+ return result
@@ -0,0 +1,96 @@
1
+ """Explicit compiler registry; JSON never imports or registers a plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+ from equiframe.contracts import Contract
9
+ from equiframe.errors import CapabilityError
10
+ from equiframe.registry import Transformation
11
+
12
+ from .types import CompilationCheck, CompilationDiagnostic, RelationalPlan
13
+ from .validation import ValidatedIR, validate_ir
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class CompiledPlan:
18
+ """Lower-level native implementation for already validated physical inputs.
19
+
20
+ Compilation checks IR/contract metadata, not data. The caller must establish
21
+ exact input types and value constraints before executing this transformation;
22
+ ``migrate`` performs that admission automatically. Passing the transformation
23
+ directly to ``execute`` uses native conversion behavior and records it, but
24
+ completion does not establish input conformance or semantic equivalence.
25
+ """
26
+
27
+ engine: str
28
+ transformation: Transformation
29
+ ir_sha256: str
30
+ contract_sha256: str
31
+ check: CompilationCheck
32
+
33
+
34
+ class PlanCompiler(Protocol):
35
+ """Compile trusted IR metadata; actual input admission belongs to the caller.
36
+
37
+ ``check`` and ``compile`` do not load or validate tables. Use ``migrate`` for
38
+ checked input capture, or independently validate exact physical input types
39
+ and constraints before using a returned ``CompiledPlan`` with ``execute``.
40
+ """
41
+
42
+ @property
43
+ def engine(self) -> str: ...
44
+
45
+ def check(self, plan: RelationalPlan, contract: Contract) -> CompilationCheck: ...
46
+
47
+ def compile(self, plan: RelationalPlan, contract: Contract) -> CompiledPlan: ...
48
+
49
+
50
+ class CompilerRegistry:
51
+ def __init__(self) -> None:
52
+ self._compilers: dict[str, PlanCompiler] = {}
53
+
54
+ def register(self, compiler: PlanCompiler) -> None:
55
+ if not compiler.engine or compiler.engine in self._compilers:
56
+ raise ValueError("compiler engine must be nonempty and registered once")
57
+ self._compilers[compiler.engine] = compiler
58
+
59
+ def resolve(self, engine: str) -> PlanCompiler:
60
+ if engine not in self._compilers:
61
+ raise CapabilityError("no explicitly registered compiler for engine: " + engine)
62
+ return self._compilers[engine]
63
+
64
+
65
+ def compiler_check(engine: str, plan: RelationalPlan, contract: Contract) -> CompilationCheck:
66
+ try:
67
+ validate_ir(plan, contract)
68
+ except (ValueError, CapabilityError) as error:
69
+ return CompilationCheck(
70
+ engine=engine,
71
+ supported=False,
72
+ diagnostics=(
73
+ CompilationDiagnostic(
74
+ code="unsupported_ir", message=str(error).strip() or "invalid IR"
75
+ ),
76
+ ),
77
+ )
78
+ return CompilationCheck(engine=engine, supported=True)
79
+
80
+
81
+ def require_supported(engine: str, plan: RelationalPlan, contract: Contract) -> ValidatedIR:
82
+ check = compiler_check(engine, plan, contract)
83
+ if not check.supported:
84
+ raise CapabilityError(check.diagnostics[0].message)
85
+ return validate_ir(plan, contract)
86
+
87
+
88
+ def native_compiler_registry() -> CompilerRegistry:
89
+ from .backends.duckdb import DuckDBCompiler
90
+ from .backends.pandas import PandasCompiler
91
+ from .backends.polars import PolarsCompiler
92
+
93
+ registry = CompilerRegistry()
94
+ for compiler in (PandasCompiler(), PolarsCompiler(), DuckDBCompiler()):
95
+ registry.register(compiler)
96
+ return registry