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,591 @@
1
+ """Bounded inspection/export of explicitly authorized comparison evidence.
2
+
3
+ Integrity means internal consistency with a supplied manifest, not independent
4
+ semantic evaluation or authentication. Inspection opens only the manifest and
5
+ JSONL; raw snapshots are read only by the separately requested export helper.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import os
13
+ from collections import Counter
14
+ from collections.abc import Iterator
15
+ from contextlib import contextmanager, suppress
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import BinaryIO, TypeVar
19
+ from urllib.parse import unquote, urlsplit
20
+ from urllib.request import url2pathname
21
+
22
+ from equiframe._base import FrozenModel
23
+ from equiframe._regular_io import absolute_path, open_regular_file, regular_directory
24
+ from equiframe.comparison_types import ComparisonDifference, ComparisonResult
25
+ from equiframe.contracts import ColumnKeyScope, SignificantOrder
26
+ from equiframe.errors import ArtifactIntegrityError, ResourceLimitError
27
+ from equiframe.types import ArtifactRef
28
+
29
+ _ModelT = TypeVar("_ModelT", bound=FrozenModel)
30
+
31
+ _MAX_DOCUMENT_BYTES = 16 * 1024 * 1024
32
+ _MAX_LINE_BYTES = 8 * 1024 * 1024
33
+ _SCALAR_CODES = frozenset(
34
+ {
35
+ "nan_forbidden",
36
+ "null_forbidden",
37
+ "integer_range",
38
+ "float_precision",
39
+ "decimal_precision",
40
+ "date_granularity",
41
+ "temporal_interval",
42
+ "naive_timestamp",
43
+ "nonexistent_time",
44
+ "ambiguous_time",
45
+ "timestamp_granularity",
46
+ "allowed_values",
47
+ }
48
+ )
49
+ _SCHEMA_CODES = frozenset({"missing_column", "additional_column", "column_order", "logical_type"})
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class EvidenceInspection:
54
+ """Verified counts; caller retains counts.final for exact/lower-bound meaning.
55
+
56
+ Added/removed refer to left candidate versus right reference. These and
57
+ modified remain unknown for bags and before alignment.
58
+ """
59
+
60
+ examples: tuple[ComparisonDifference, ...]
61
+ records: int
62
+ violations: int
63
+ added: int | None
64
+ removed: int | None
65
+ modified: int | None
66
+ code_counts: tuple[tuple[str, int], ...]
67
+ candidate_excess: int
68
+ reference_excess: int
69
+
70
+
71
+ class EvidencePathError(ArtifactIntegrityError):
72
+ """A comparison evidence path or file type violates confinement requirements."""
73
+
74
+
75
+ def _require(condition: bool, message: str) -> None:
76
+ if not condition:
77
+ raise ArtifactIntegrityError(message)
78
+
79
+
80
+ def _path_require(condition: bool, message: str) -> None:
81
+ if not condition:
82
+ raise EvidencePathError(message)
83
+
84
+
85
+ def _limits(max_bytes: int, max_records: int, max_examples: int) -> None:
86
+ for value, name, minimum in (
87
+ (max_bytes, "max_bytes", 1),
88
+ (max_records, "max_records", 1),
89
+ (max_examples, "max_examples", 0),
90
+ ):
91
+ if type(value) is not int or value < minimum:
92
+ raise ValueError(f"{name} must be an integer >= {minimum}")
93
+
94
+
95
+ def _absolute(path: Path) -> Path:
96
+ return absolute_path(path, path_error=EvidencePathError)
97
+
98
+
99
+ def _directory(path: Path) -> Path:
100
+ return regular_directory(path, path_error=EvidencePathError)
101
+
102
+
103
+ def _reference_path(ref: ArtifactRef, root: Path) -> Path:
104
+ try:
105
+ parts = urlsplit(ref.uri)
106
+ except ValueError:
107
+ raise EvidencePathError("evidence URI is invalid") from None
108
+ _path_require(
109
+ parts.scheme == "file" and not parts.netloc and not parts.query and not parts.fragment,
110
+ "evidence requires a local file URI without a host",
111
+ )
112
+ try:
113
+ decoded = unquote(parts.path, errors="strict")
114
+ except UnicodeError:
115
+ raise EvidencePathError("evidence path encoding is invalid") from None
116
+ _path_require(
117
+ not decoded.startswith("//") and "\\" not in decoded and "\x00" not in decoded,
118
+ "network or malformed evidence paths are not permitted",
119
+ )
120
+ path = Path(url2pathname(parts.path))
121
+ _path_require(path.is_absolute(), "evidence paths must be absolute")
122
+ path = _absolute(path)
123
+ _path_require(path.is_relative_to(root), "evidence reference is outside the authorized root")
124
+ return path
125
+
126
+
127
+ @contextmanager
128
+ def _open_regular(path: Path) -> Iterator[BinaryIO]:
129
+ """Check every ancestor, reject special files, and pin POSIX directories.
130
+
131
+ On systems without openat/O_NOFOLLOW, identities are checked around open.
132
+ These cooperative checks do not sandbox a hostile concurrent filesystem.
133
+ """
134
+ with open_regular_file(path, path_error=EvidencePathError) as stream:
135
+ yield stream
136
+
137
+
138
+ def _object(pairs: list[tuple[str, object]]) -> dict[str, object]:
139
+ result: dict[str, object] = {}
140
+ for key, value in pairs:
141
+ _require(key not in result, "duplicate JSON object member in evidence")
142
+ result[key] = value
143
+ return result
144
+
145
+
146
+ def _constant(value: str) -> object:
147
+ raise ArtifactIntegrityError("nonstandard JSON number in evidence")
148
+
149
+
150
+ def _document(raw: bytes, model: type[_ModelT]) -> _ModelT:
151
+ try:
152
+ text = raw.decode("utf-8")
153
+ parsed = json.loads(text, object_pairs_hook=_object, parse_constant=_constant)
154
+ _require(isinstance(parsed, dict), "evidence JSON must be an object")
155
+ return model.model_validate_json(text)
156
+ except (UnicodeError, ValueError, RecursionError) as error:
157
+ raise ArtifactIntegrityError("invalid typed evidence JSON") from error
158
+
159
+
160
+ def _read_document(path: Path, model: type[_ModelT], budget: int) -> tuple[_ModelT, int]:
161
+ with _open_regular(path) as source:
162
+ limit = min(budget, _MAX_DOCUMENT_BYTES)
163
+ if os.fstat(source.fileno()).st_size > limit:
164
+ raise ResourceLimitError("manifest exceeds report byte budget")
165
+ raw = source.read(limit + 1)
166
+ if len(raw) > limit:
167
+ raise ResourceLimitError("manifest exceeds report byte budget")
168
+ return _document(raw, model), len(raw)
169
+
170
+
171
+ def _manifest(result: ComparisonResult, root: Path, max_bytes: int) -> tuple[Path, int]:
172
+ root = _directory(root)
173
+ path = _reference_path(result.differences, root)
174
+ _path_require(
175
+ path.name == "differences.jsonl" and path.parent.name == result.comparison_id,
176
+ "evidence must use the comparison identifier and conventional filenames",
177
+ )
178
+ persisted, size = _read_document(path.with_name("comparison.json"), ComparisonResult, max_bytes)
179
+ _require(persisted == result, "persisted comparison manifest differs from supplied result")
180
+ return path, size
181
+
182
+
183
+ def _validate_difference(difference: ComparisonDifference, result: ComparisonResult) -> None:
184
+ schema = result.spec.contract.output.schema_spec
185
+ names = {column.name for column in schema.columns}
186
+ width = len(names)
187
+ rows = {"left": result.counts.left_rows, "right": result.counts.right_rows}
188
+ d = difference
189
+ if d.category == "content":
190
+ _require(result.strategy != "pending", "content evidence requires an alignment strategy")
191
+ _require(d.side is None, "content differences must not carry a violation side")
192
+ for position, count, values, side in (
193
+ (d.position, d.left_count, d.left, "left"),
194
+ (d.other_position, d.right_count, d.right, "right"),
195
+ ):
196
+ _require((position is not None) == (count > 0), "content count/position mismatch")
197
+ _require(bool(values) == (count > 0), "content count/value mismatch")
198
+ _require(count <= rows[side], "content count exceeds observed rows")
199
+ if position is not None:
200
+ _require(position < rows[side], "content position exceeds observed rows")
201
+ if d.code == "cell_value":
202
+ _require(result.strategy in ("key", "sequence"), "cell differences require alignment")
203
+ _require(d.column in names, "cell column is not declared")
204
+ _require(
205
+ d.left_count == d.right_count == len(d.left) == len(d.right) == 1,
206
+ "cell difference must contain two single values",
207
+ )
208
+ if result.strategy == "key":
209
+ key = next(key for key in schema.keys if key.name == result.selected_key)
210
+ key_width = len(key.columns)
211
+ if isinstance(key.scope, ColumnKeyScope):
212
+ key_width += len(key.scope.columns)
213
+ _require(len(d.key) == key_width, "cell key dimensions do not match selected key")
214
+ else:
215
+ _require(not d.key, "sequence differences must not claim a key")
216
+ _require(d.position == d.other_position, "sequence cell positions must align")
217
+ elif d.code == "row_multiplicity":
218
+ _require(d.column is None and not d.key, "row differences have no column or key")
219
+ _require(d.left_count != d.right_count, "row difference must change multiplicity")
220
+ _require(
221
+ (not d.left or len(d.left) == width) and (not d.right or len(d.right) == width),
222
+ "row value dimensions do not match declared columns",
223
+ )
224
+ if result.strategy != "multiset":
225
+ _require(
226
+ (d.left_count, d.right_count) in ((1, 0), (0, 1)),
227
+ "aligned/matching unmatched rows must be single-sided occurrences",
228
+ )
229
+ else:
230
+ raise ArtifactIntegrityError("unknown content difference code")
231
+ return
232
+ _require(d.side is not None, "contract/schema evidence requires a side")
233
+ _require(
234
+ not d.key and not d.left_count and not d.right_count and d.other_position is None,
235
+ "violation must not claim aligned row counts or keys",
236
+ )
237
+ _require(
238
+ not (d.right if d.side == "left" else d.left),
239
+ "violation values must belong to their declared side",
240
+ )
241
+ values = d.left if d.side == "left" else d.right
242
+ if d.category == "schema":
243
+ _require(d.code in _SCHEMA_CODES, "unknown schema difference code")
244
+ _require(d.position is None and not values, "schema evidence must not claim row values")
245
+ if d.code == "column_order":
246
+ _require(d.column is None, "column order evidence must not select one column")
247
+ elif d.code == "additional_column":
248
+ _require(d.column is not None and d.column not in names, "invalid additional column")
249
+ else:
250
+ _require(d.column in names, "schema column is not declared")
251
+ return
252
+ if not (d.side is not None):
253
+ raise AssertionError("Internal invariant check failed.")
254
+ _require(d.position is not None, "contract evidence requires a row position")
255
+ if not (d.position is not None):
256
+ raise AssertionError("Internal invariant check failed.")
257
+ # Ingest emits violations before incrementing its row count. Interruption
258
+ # can retain a violation at exactly the next observed position.
259
+ _require(
260
+ d.position < rows[d.side] or (not result.complete and d.position == rows[d.side]),
261
+ "violation position exceeds observed rows",
262
+ )
263
+ if d.code in _SCALAR_CODES:
264
+ _require(d.column in names and len(values) == 1, "scalar violation dimensions mismatch")
265
+ return
266
+ _require(d.column is None, "row/key violations must not select one column")
267
+ if d.code in ("order_violation", "order_tie"):
268
+ _require(isinstance(schema.order, SignificantOrder), "order violation needs ordered policy")
269
+ _require(len(values) == width, "order violation row dimensions mismatch")
270
+ return
271
+ if d.code == "duplicate_normalized_row":
272
+ _require(schema.multiplicity == "distinct", "duplicate row needs distinct policy")
273
+ _require(len(values) == width, "duplicate row dimensions mismatch")
274
+ return
275
+ for key in schema.keys:
276
+ key_width = len(key.columns)
277
+ if isinstance(key.scope, ColumnKeyScope):
278
+ key_width += len(key.scope.columns)
279
+ incomplete = {f"incomplete_{phase}_{key.name}" for phase in ("raw", "normalized")}
280
+ duplicate = {f"duplicate_{phase}_key_{key.name}" for phase in ("raw", "normalized")}
281
+ if d.code in incomplete | duplicate:
282
+ _require(len(values) == key_width, "key violation dimensions mismatch")
283
+ _require(
284
+ key.incomplete == "reject" if d.code in incomplete else key.unique,
285
+ "key violation requires the corresponding declared policy",
286
+ )
287
+ return
288
+ raise ArtifactIntegrityError("unknown contract difference code")
289
+
290
+
291
+ def inspect_evidence(
292
+ result: ComparisonResult,
293
+ root: Path,
294
+ *,
295
+ max_bytes: int,
296
+ max_records: int,
297
+ max_examples: int,
298
+ ) -> EvidenceInspection:
299
+ """Verify all bounded persisted records without opening source/execution paths.
300
+
301
+ The aggregate byte budget includes the manifest and JSONL. Oversized lines
302
+ or manifests fail instead of returning truncated verification. Examples are
303
+ the requested prefix, independent of how many persisted records are checked.
304
+ """
305
+ _limits(max_bytes, max_records, max_examples)
306
+ result = ComparisonResult.model_validate(result)
307
+ path, manifest_size = _manifest(result, root, max_bytes)
308
+ ref = result.differences
309
+ remaining = max_bytes - manifest_size
310
+ if ref.size_bytes > remaining or result.counts.difference_records > max_records:
311
+ raise ResourceLimitError("comparison evidence exceeds report inspection budget")
312
+ digest = hashlib.sha256()
313
+ size = records = violations = added = removed = 0
314
+ codes: Counter[str] = Counter()
315
+ examples: list[ComparisonDifference] = []
316
+ modified: set[tuple[int, int]] = set()
317
+ cells: set[tuple[int, int, str]] = set()
318
+ left_positions: dict[int, int | None] = {}
319
+ right_positions: dict[int, int | None] = {}
320
+ with _open_regular(path) as source:
321
+ _require(os.fstat(source.fileno()).st_size == ref.size_bytes, "difference size mismatch")
322
+ while True:
323
+ line = source.readline(min(_MAX_LINE_BYTES, remaining - size) + 1)
324
+ if not line:
325
+ break
326
+ size += len(line)
327
+ if size > remaining or len(line) > _MAX_LINE_BYTES:
328
+ raise ResourceLimitError("difference line/total exceeds report byte budget")
329
+ records += 1
330
+ if records > max_records:
331
+ raise ResourceLimitError("difference records exceed report record budget")
332
+ digest.update(line)
333
+ difference = _document(line, ComparisonDifference)
334
+ _validate_difference(difference, result)
335
+ if records <= len(result.examples):
336
+ _require(
337
+ difference == result.examples[records - 1],
338
+ "manifest examples do not match the persisted prefix",
339
+ )
340
+ if records <= max_examples:
341
+ examples.append(difference)
342
+ codes[difference.code] += 1
343
+ violations += difference.category in ("contract", "schema")
344
+ if difference.category != "content":
345
+ continue
346
+ a, b = difference.position, difference.other_position
347
+ if difference.code == "cell_value":
348
+ if not (a is not None and b is not None and difference.column is not None):
349
+ raise AssertionError("Internal invariant check failed.")
350
+ cell = (a, b, difference.column)
351
+ _require(cell not in cells, "duplicate aligned cell evidence")
352
+ cells.add(cell)
353
+ _require(
354
+ left_positions.get(a, b) == b and right_positions.get(b, a) == a,
355
+ "content positions do not define a one-to-one alignment",
356
+ )
357
+ left_positions[a], right_positions[b] = b, a
358
+ modified.add((a, b))
359
+ else:
360
+ for position, positions in ((a, left_positions), (b, right_positions)):
361
+ if position is not None:
362
+ _require(position not in positions, "repeated unmatched row position")
363
+ positions[position] = None
364
+ added += max(0, difference.left_count - difference.right_count)
365
+ removed += max(0, difference.right_count - difference.left_count)
366
+ _require(
367
+ size == ref.size_bytes and digest.hexdigest() == ref.sha256, "difference hash/size mismatch"
368
+ )
369
+ _require(records == result.counts.difference_records, "difference record count mismatch")
370
+ _require(violations == result.counts.contract_violations, "contract violation count mismatch")
371
+ _require(
372
+ result.counts.matched_rows + len(modified) + added <= result.counts.left_rows
373
+ and result.counts.matched_rows + len(modified) + removed <= result.counts.right_rows,
374
+ "content evidence exceeds observed row counts",
375
+ )
376
+ if result.complete:
377
+ _require(
378
+ result.counts.matched_rows + len(modified) + added == result.counts.left_rows
379
+ and result.counts.matched_rows + len(modified) + removed == result.counts.right_rows,
380
+ "content evidence does not account for completed row counts",
381
+ )
382
+ aligned = result.strategy in ("key", "sequence")
383
+ return EvidenceInspection(
384
+ examples=tuple(examples),
385
+ records=records,
386
+ violations=violations,
387
+ added=added if aligned else None,
388
+ removed=removed if aligned else None,
389
+ modified=len(modified) if aligned else None,
390
+ code_counts=tuple(sorted(codes.items())),
391
+ candidate_excess=added,
392
+ reference_excess=removed,
393
+ )
394
+
395
+
396
+ def _json_bytes(value: object) -> bytes:
397
+ return json.dumps(
398
+ value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False
399
+ ).encode("utf-8")
400
+
401
+
402
+ def _inventory(path: Path, digest: str, size: int) -> dict[str, object]:
403
+ return {"path": path.name, "sha256": digest, "size_bytes": size}
404
+
405
+
406
+ def _write_private(path: Path, data: bytes) -> dict[str, object]:
407
+ descriptor = os.open(
408
+ path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600
409
+ )
410
+ with os.fdopen(descriptor, "wb") as target:
411
+ target.write(data)
412
+ target.flush()
413
+ os.fsync(target.fileno())
414
+ return _inventory(path, hashlib.sha256(data).hexdigest(), len(data))
415
+
416
+
417
+ def _copy_capture(
418
+ ref: ArtifactRef, root: Path, destination: Path, budget: int
419
+ ) -> dict[str, object]:
420
+ _require(ref.format in ("arrow", "parquet"), "reproduction captures require Arrow/Parquet")
421
+ if ref.size_bytes > budget:
422
+ raise ResourceLimitError("reproduction captures exceed export byte budget")
423
+ source_path = _reference_path(ref, root)
424
+ digest = hashlib.sha256()
425
+ size = 0
426
+ with _open_regular(source_path) as source:
427
+ _require(os.fstat(source.fileno()).st_size == ref.size_bytes, "capture size mismatch")
428
+ descriptor = os.open(
429
+ destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600
430
+ )
431
+ with os.fdopen(descriptor, "wb") as target:
432
+ while block := source.read(min(1024 * 1024, budget - size + 1)):
433
+ size += len(block)
434
+ if size > budget:
435
+ raise ResourceLimitError("reproduction captures exceed export byte budget")
436
+ digest.update(block)
437
+ target.write(block)
438
+ target.flush()
439
+ os.fsync(target.fileno())
440
+ _require(
441
+ size == ref.size_bytes and digest.hexdigest() == ref.sha256, "capture hash/size mismatch"
442
+ )
443
+ return _inventory(destination, digest.hexdigest(), size)
444
+
445
+
446
+ def export_reproduction(
447
+ result: ComparisonResult,
448
+ root: Path,
449
+ destination: Path,
450
+ *,
451
+ max_bytes: int,
452
+ max_records: int,
453
+ ) -> dict[str, object]:
454
+ """Export confined captured outputs and context, never execute transformations.
455
+
456
+ The caller must separately authorize full data/metadata disclosure. The new
457
+ destination is private on POSIX and must not exist. Only a final
458
+ reproduction.json marks successful publication; failures remove files this
459
+ helper created. This is materialized-comparison context, not original code,
460
+ execution inputs, a promised deterministic replay, or an authenticated proof.
461
+ """
462
+ from equiframe.diff import _DiffReceipt
463
+
464
+ _limits(max_bytes, max_records, 0)
465
+ result = ComparisonResult.model_validate(result)
466
+ _require(result.complete, "reproduction requires a complete comparison")
467
+ _require(
468
+ result.left is not None and result.right is not None,
469
+ "reproduction requires both captured outputs",
470
+ )
471
+ if not (result.left is not None and result.right is not None):
472
+ raise AssertionError("Internal invariant check failed.")
473
+ _require(
474
+ result.left.execution is None and result.right.execution is None,
475
+ "execution-backed sources require their independent execution context; export unsupported",
476
+ )
477
+ inspect_evidence(result, root, max_bytes=max_bytes, max_records=max_records, max_examples=0)
478
+ root = _directory(root)
479
+ evidence_path, consumed = _manifest(result, root, max_bytes)
480
+ consumed += result.differences.size_bytes
481
+ context: dict[str, object] = {
482
+ "schema_version": "1.0",
483
+ "scope": "materialized_comparison_only_no_transform_reexecution",
484
+ "spec": result.spec.model_dump(mode="json"),
485
+ "options": result.options.model_dump(mode="json"),
486
+ "reference": result.reference,
487
+ "versions": [item.model_dump(mode="json") for item in result.versions],
488
+ }
489
+ companion = evidence_path.with_name("diff-contract.json")
490
+ try:
491
+ companion.lstat()
492
+ except FileNotFoundError:
493
+ pass
494
+ else:
495
+ receipt, size = _read_document(companion, _DiffReceipt, max_bytes - consumed)
496
+ consumed += size
497
+ _require(
498
+ receipt.comparison_id == result.comparison_id
499
+ and receipt.spec_sha256 == result.spec_sha256
500
+ and receipt.declaration.spec == result.spec
501
+ and receipt.declaration.reference == result.reference,
502
+ "diff declaration does not match the comparison",
503
+ )
504
+ context["diff_declaration"] = receipt.declaration.model_dump(mode="json")
505
+ context["capture_options"] = receipt.capture_options.model_dump(mode="json")
506
+ destination = _absolute(destination)
507
+ _directory(destination.parent)
508
+ files: list[dict[str, object]] = []
509
+ created: list[Path] = []
510
+ made_directory = False
511
+ try:
512
+ destination.mkdir(mode=0o700)
513
+ made_directory = True
514
+ for side, source in (("left", result.left), ("right", result.right)):
515
+ captured = destination / f"{side}.{source.data.format}"
516
+ created.append(captured)
517
+ item = _copy_capture(source.data, evidence_path.parent, captured, max_bytes - consumed)
518
+ consumed += source.data.size_bytes
519
+ files.append(item)
520
+ capture: dict[str, object] = {"data": item | {"format": source.data.format}}
521
+ if source.table is not None:
522
+ table = source.table
523
+ _require(
524
+ (table.data.sha256, table.data.size_bytes, table.data.format)
525
+ == (source.data.sha256, source.data.size_bytes, source.data.format),
526
+ "table context does not identify the captured output",
527
+ )
528
+ _require(
529
+ table.name == result.spec.contract.output.name
530
+ and table.row_count
531
+ == (result.counts.left_rows if side == "left" else result.counts.right_rows)
532
+ and tuple((column.name, column.arrow_type) for column in table.columns)
533
+ == source.observed_schema,
534
+ "table context does not match observed schema/rows",
535
+ )
536
+ capture["table"] = {
537
+ "name": table.name,
538
+ "row_count": table.row_count,
539
+ "columns": [column.model_dump(mode="json") for column in table.columns],
540
+ "index_columns": [
541
+ column.model_dump(mode="json") for column in table.index_columns
542
+ ],
543
+ }
544
+ if table.index is not None:
545
+ index_path = destination / f"{side}-index.{table.index.format}"
546
+ created.append(index_path)
547
+ index_item = _copy_capture(
548
+ table.index, evidence_path.parent, index_path, max_bytes - consumed
549
+ )
550
+ consumed += table.index.size_bytes
551
+ files.append(index_item)
552
+ capture["index"] = index_item | {"format": table.index.format}
553
+ context[side] = capture
554
+ data = _json_bytes(context)
555
+ if consumed + len(data) > max_bytes or len(data) > _MAX_DOCUMENT_BYTES:
556
+ raise ResourceLimitError("reproduction context exceeds export byte budget")
557
+ context_path = destination / "context.json"
558
+ created.append(context_path)
559
+ files.append(_write_private(context_path, data))
560
+ consumed += len(data)
561
+ manifest: dict[str, object] = {
562
+ "schema_version": "1.0",
563
+ "scope": "materialized_comparison_only_no_transform_reexecution",
564
+ "complete": True,
565
+ "files": files,
566
+ "context": "context.json",
567
+ "source_state": result.state,
568
+ "source_complete": result.complete,
569
+ "source_spec_sha256": result.spec_sha256,
570
+ "source_contract_sha256": result.contract_sha256,
571
+ "integrity": "hashes_identify_bytes_not_authentication_or_semantic_proof",
572
+ }
573
+ manifest_bytes = _json_bytes(manifest)
574
+ if consumed + len(manifest_bytes) > max_bytes:
575
+ raise ResourceLimitError("reproduction manifest exceeds export byte budget")
576
+ _require(
577
+ {path.name for path in destination.iterdir()} == {item["path"] for item in files},
578
+ "reproduction destination contains files outside the closed inventory",
579
+ )
580
+ final_path = destination / "reproduction.json"
581
+ created.append(final_path)
582
+ _write_private(final_path, manifest_bytes)
583
+ return manifest
584
+ except BaseException:
585
+ for path in reversed(created):
586
+ with suppress(OSError):
587
+ path.unlink(missing_ok=True)
588
+ if made_directory:
589
+ with suppress(OSError):
590
+ destination.rmdir()
591
+ raise