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,271 @@
1
+ """Fail-closed assertions and integration decisions over existing comparisons.
2
+
3
+ Nothing here compares data again, changes its policy or authenticates a producer.
4
+ By default no evidence files are opened. Optional bounded verification checks
5
+ only the current comparison manifest and its complete differences, under an
6
+ explicit caller-authorized root. Reports must be written separately beforehand.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import errno
12
+ import json
13
+ from pathlib import Path
14
+ from typing import cast
15
+
16
+ from equiframe.assertion_types import (
17
+ EquivalenceCode,
18
+ EquivalenceDecision,
19
+ EquivalencePolicy,
20
+ EvidenceFailureCode,
21
+ EvidenceStatus,
22
+ GuaranteeFailure,
23
+ )
24
+ from equiframe.comparison_report import comparison_report
25
+ from equiframe.comparison_types import ComparisonResult
26
+ from equiframe.errors import (
27
+ ArtifactIntegrityError,
28
+ ArtifactIOError,
29
+ EquiFrameError,
30
+ ResourceLimitError,
31
+ )
32
+ from equiframe.report_types import ReportOptions
33
+
34
+
35
+ class EquivalenceInputError(ValueError, EquiFrameError):
36
+ """Invalid caller input or unsupported assertion configuration, not a verdict."""
37
+
38
+ code = "invalid_assertion_input"
39
+
40
+
41
+ class EquivalenceAssertionError(AssertionError, EquiFrameError):
42
+ """Safe failure carrying a detached decision, never the raw comparison data."""
43
+
44
+ code = "equivalence_not_accepted"
45
+
46
+ def __init__(self, decision: EquivalenceDecision, *, report_path: Path | None = None) -> None:
47
+ self.decision = decision
48
+ self.code = decision.code
49
+ messages = {
50
+ "different": "Comparison differs; review the report and declared policies",
51
+ "not_evaluable": "Comparison is not evaluable; resolve the diagnostic and rerun",
52
+ "guarantee_mismatch": "Required comparison context differs; review policy pins",
53
+ "evidence_missing": "Required evidence is missing; restore finalized comparison files",
54
+ "evidence_corrupt": "Required evidence failed integrity checks; restore or regenerate",
55
+ "evidence_path_rejected": "Evidence path was rejected; use a confined local root",
56
+ "evidence_budget_exceeded": "Evidence verification exceeded its budget; review limits",
57
+ "evidence_unreadable": "Required evidence is unreadable; check local read access",
58
+ }
59
+ message = f"[{decision.code}] {messages.get(decision.code, 'Equivalence was not accepted')}"
60
+ if decision.safe_reason_code is not None:
61
+ message += f" (reason: {decision.safe_reason_code})"
62
+ if report_path is not None:
63
+ # This is a trusted pointer supplied by the caller, not a result URI.
64
+ # No resolution or file access. ASCII JSON escapes terminal controls.
65
+ escaped = json.dumps(str(report_path), ensure_ascii=True)
66
+ pointer = escaped if len(escaped) <= 240 else escaped[:236] + '..."'
67
+ message += f"; report: {pointer}"
68
+ super().__init__(message)
69
+
70
+
71
+ class EquivalenceDifferentError(EquivalenceAssertionError):
72
+ """A completed comparison found differences or contract violations."""
73
+
74
+ code = "different"
75
+
76
+
77
+ class EquivalenceNotEvaluableError(EquivalenceAssertionError):
78
+ """A comparison cannot establish equivalence, even if traversal completed."""
79
+
80
+ code = "not_evaluable"
81
+
82
+
83
+ class EquivalenceGuaranteeError(EquivalenceAssertionError):
84
+ """Observed equivalence does not satisfy the caller's pinned context."""
85
+
86
+ code = "guarantee_mismatch"
87
+
88
+
89
+ class EquivalenceEvidenceError(EquivalenceAssertionError):
90
+ """Observed equivalence lacks the required verified persisted evidence."""
91
+
92
+ code = "equivalence_evidence_error"
93
+
94
+
95
+ def _validated(
96
+ result: ComparisonResult, policy: EquivalencePolicy | None, evidence_root: Path | None
97
+ ) -> tuple[ComparisonResult, EquivalencePolicy]:
98
+ __tracebackhide__ = True
99
+ if not isinstance(result, ComparisonResult):
100
+ raise EquivalenceInputError(
101
+ "[invalid_assertion_input] result must be a ComparisonResult, not a report or mapping"
102
+ ) from None
103
+ if policy is not None and not isinstance(policy, EquivalencePolicy):
104
+ raise EquivalenceInputError(
105
+ "[invalid_assertion_input] policy must be an EquivalencePolicy or None"
106
+ ) from None
107
+ if evidence_root is not None and not isinstance(evidence_root, Path):
108
+ raise EquivalenceInputError(
109
+ "[invalid_assertion_input] evidence_root must be an explicit pathlib.Path"
110
+ ) from None
111
+ if evidence_root is not None and (
112
+ "\x00" in str(evidence_root)
113
+ or any(0xD800 <= ord(char) <= 0xDFFF for char in str(evidence_root))
114
+ ):
115
+ raise EquivalenceInputError(
116
+ "[invalid_assertion_input] evidence_root must contain a valid local path"
117
+ ) from None
118
+ try:
119
+ # FrozenModel revalidates instances recursively, including model_copy and
120
+ # model_construct bypasses. Do not coerce mappings or serialized reports.
121
+ result = ComparisonResult.model_validate(result, strict=True)
122
+ policy = EquivalencePolicy.model_validate(
123
+ policy if policy is not None else EquivalencePolicy(), strict=True
124
+ )
125
+ except (ValueError, TypeError, AttributeError, RecursionError):
126
+ raise EquivalenceInputError(
127
+ "[invalid_assertion_input] invalid comparison result or assertion policy; "
128
+ "recreate it through the typed API"
129
+ ) from None
130
+ if policy.require_persisted_evidence != (evidence_root is not None):
131
+ raise EquivalenceInputError(
132
+ "[invalid_assertion_input] require_persisted_evidence and an explicit evidence_root "
133
+ "must be supplied together"
134
+ ) from None
135
+ return result, policy
136
+
137
+
138
+ def _inspect(
139
+ result: ComparisonResult, policy: EquivalencePolicy, root: Path | None
140
+ ) -> tuple[EvidenceStatus, EvidenceFailureCode | None]:
141
+ __tracebackhide__ = True
142
+ if root is None:
143
+ return "not_requested", None
144
+ from equiframe._report_io import EvidencePathError, inspect_evidence
145
+
146
+ try:
147
+ inspect_evidence(
148
+ result,
149
+ root,
150
+ max_bytes=policy.max_evidence_bytes,
151
+ max_records=policy.max_records,
152
+ max_examples=0,
153
+ )
154
+ except EvidencePathError:
155
+ return "path_rejected", "evidence_path_rejected"
156
+ except ResourceLimitError:
157
+ return "budget_exceeded", "evidence_budget_exceeded"
158
+ except ArtifactIOError as error:
159
+ cause = error.__cause__
160
+ if isinstance(cause, FileNotFoundError):
161
+ return "missing", "evidence_missing"
162
+ if isinstance(cause, OSError) and cause.errno in (errno.ELOOP, errno.ENOTDIR):
163
+ return "path_rejected", "evidence_path_rejected"
164
+ return "unreadable", "evidence_unreadable"
165
+ except ArtifactIntegrityError:
166
+ return "corrupt", "evidence_corrupt"
167
+ return "verified", None
168
+
169
+
170
+ def _safe_reason(result: ComparisonResult) -> str | None:
171
+ __tracebackhide__ = True
172
+ if result.state == "equivalent":
173
+ return None
174
+ try:
175
+ report = comparison_report(result, options=ReportOptions(max_examples=1))
176
+ except Exception:
177
+ # Presentation is best-effort, with no authority over acceptance. A
178
+ # formatting failure or budget must not mask the comparison verdict.
179
+ return None
180
+ if report.reasons:
181
+ return report.reasons[0].partition(":")[0]
182
+ return report.examples[0].code if report.examples else None
183
+
184
+
185
+ def equivalence_decision(
186
+ result: ComparisonResult,
187
+ *,
188
+ policy: EquivalencePolicy | None = None,
189
+ evidence_root: Path | None = None,
190
+ ) -> EquivalenceDecision:
191
+ """Return explicit acceptance of a validated, full output-schema comparison.
192
+
193
+ Only equivalent + complete + final counts can pass. Both peer and expected
194
+ references establish only observed_contract equivalence. Optional pins and
195
+ persisted evidence can tighten acceptance, never change comparison truth.
196
+
197
+ Invalid inputs/configuration raise EquivalenceInputError. Other failures are
198
+ recorded without raising. Precedence is original state, guarantee pins, then
199
+ evidence; all requested checks remain visible on the returned decision.
200
+ Supplying evidence_root requires policy.require_persisted_evidence=True.
201
+ """
202
+ __tracebackhide__ = True
203
+ result, policy = _validated(result, policy, evidence_root)
204
+ failures: list[GuaranteeFailure] = []
205
+ for field, required, observed in (
206
+ ("required_contract_sha256", policy.required_contract_sha256, result.contract_sha256),
207
+ ("required_spec_sha256", policy.required_spec_sha256, result.spec_sha256),
208
+ ("required_reference", policy.required_reference, result.reference),
209
+ ):
210
+ if required is not None and required != observed:
211
+ failures.append(cast(GuaranteeFailure, field))
212
+ evidence_status, evidence_code = _inspect(result, policy, evidence_root)
213
+ code: EquivalenceCode = (
214
+ result.state
215
+ if result.state != "equivalent"
216
+ else "guarantee_mismatch"
217
+ if failures
218
+ else evidence_code or "equivalent"
219
+ )
220
+ return EquivalenceDecision(
221
+ accepted=code == "equivalent",
222
+ code=code,
223
+ state=result.state,
224
+ complete=result.complete,
225
+ counts_final=result.counts.final,
226
+ scope=result.scope,
227
+ reference=result.reference,
228
+ contract_sha256=result.contract_sha256,
229
+ spec_sha256=result.spec_sha256,
230
+ guarantee=policy.guarantee,
231
+ guarantee_conforms=not failures,
232
+ guarantee_failures=tuple(failures),
233
+ evidence_status=evidence_status,
234
+ evidence_code=evidence_code,
235
+ safe_reason_code=_safe_reason(result),
236
+ )
237
+
238
+
239
+ def assert_equivalent(
240
+ result: ComparisonResult,
241
+ *,
242
+ policy: EquivalencePolicy | None = None,
243
+ evidence_root: Path | None = None,
244
+ report_path: Path | None = None,
245
+ ) -> None:
246
+ """Return None on accepted equivalence; otherwise raise a safe typed assertion.
247
+
248
+ Write any desired report before calling. report_path is only a caller-trusted,
249
+ escaped, bounded pointer displayed on failure; no report is written or read.
250
+ Values, names, producer diagnostics and result paths are never displayed.
251
+ Pytest can hide helper frames; a caller's own source/locals and separately
252
+ published artifacts remain the caller's responsibility.
253
+ """
254
+ __tracebackhide__ = True
255
+ if report_path is not None and not isinstance(report_path, Path):
256
+ raise EquivalenceInputError(
257
+ "[invalid_assertion_input] report_path must be a pathlib.Path or None"
258
+ ) from None
259
+ decision = equivalence_decision(result, policy=policy, evidence_root=evidence_root)
260
+ if decision.accepted:
261
+ return
262
+ error_type = (
263
+ EquivalenceDifferentError
264
+ if decision.code == "different"
265
+ else EquivalenceNotEvaluableError
266
+ if decision.code == "not_evaluable"
267
+ else EquivalenceGuaranteeError
268
+ if decision.code == "guarantee_mismatch"
269
+ else EquivalenceEvidenceError
270
+ )
271
+ raise error_type(decision, report_path=report_path) from None
equiframe/benchmark.py ADDED
@@ -0,0 +1,101 @@
1
+ """Quarantine for the removed, unsupported auxiliary benchmark draft.
2
+
3
+ The former implementation supplied placeholder metrics, reused training profiles
4
+ for calibration, and mislabeled row-count comparisons as R. It is not scientific
5
+ evidence. The old function names remain only to give callers an actionable error;
6
+ none executes, reads inputs, fits a detector, computes metrics, or writes files.
7
+ The independent ``equiframe_benchmark`` distribution is not a drop-in replacement.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, NoReturn
15
+
16
+ if TYPE_CHECKING:
17
+ from equiframe import (
18
+ DecisionState,
19
+ DetectorBundle,
20
+ DetectorSupport,
21
+ ExecutionResult,
22
+ IsolationForestConfig,
23
+ ProfileVector,
24
+ )
25
+
26
+ _UNSUPPORTED_MESSAGE = (
27
+ "equiframe.benchmark is quarantined: the unsupported auxiliary draft has been "
28
+ "removed and cannot produce benchmark results or evidence. Use the separate "
29
+ "equiframe_benchmark package following packages/equiframe-benchmark/README.md "
30
+ "(Instalar y ejecutar), then run 'equiframe-benchmark --help'. This is not a "
31
+ "drop-in replacement; independent review and experimental protocol requirements "
32
+ "still apply."
33
+ )
34
+
35
+
36
+ class UnsupportedBenchmarkError(RuntimeError):
37
+ """An attempt to use the quarantined auxiliary benchmark draft."""
38
+
39
+
40
+ def run_benchmark(engines: list[str], families: list[str], work_dir: Path) -> NoReturn:
41
+ """Reject the unsupported draft without running ETL or creating artifacts."""
42
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
43
+
44
+
45
+ def collect_profile_vectors(work_dir: Path, families: list[str], engines: list[str]) -> NoReturn:
46
+ """Reject the unsupported draft without reading any profiles."""
47
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
48
+
49
+
50
+ def fit_isolation_forest_mad(
51
+ training_vectors: list[ProfileVector],
52
+ config: IsolationForestConfig | None = None,
53
+ support: DetectorSupport | None = None,
54
+ ) -> NoReturn:
55
+ """Reject the unsupported draft without fitting or calibrating detectors."""
56
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
57
+
58
+
59
+ def detect_anomalies(bundle: DetectorBundle, vectors: list[ProfileVector]) -> NoReturn:
60
+ """Reject the unsupported draft without issuing detector decisions."""
61
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
62
+
63
+
64
+ def rule_divergence(results: dict[str, dict[str, list[ExecutionResult]]]) -> NoReturn:
65
+ """Reject the removed row-count proxy; it did not implement rules R."""
66
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
67
+
68
+
69
+ def compute_metrics(
70
+ rule_diverge: dict[str, list[bool]],
71
+ model_decisions: list[DecisionState],
72
+ engines: list[str],
73
+ ) -> NoReturn:
74
+ """Reject the removed placeholder metrics; no metric values are returned."""
75
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
76
+
77
+
78
+ def write_summary_tables(
79
+ work_dir: Path,
80
+ results: dict[str, dict[str, list[ExecutionResult]]],
81
+ profile_vectors: list[tuple[str, str, ProfileVector]],
82
+ m_decisions: list[DecisionState],
83
+ b_decisions: list[DecisionState],
84
+ rule_diverge: dict[str, list[bool]],
85
+ engines: list[str],
86
+ ) -> NoReturn:
87
+ """Reject the unsupported draft without creating or modifying any files."""
88
+ raise UnsupportedBenchmarkError(_UNSUPPORTED_MESSAGE)
89
+
90
+
91
+ def main() -> NoReturn:
92
+ """Explain quarantine and exit 2; only --help exits successfully."""
93
+ parser = argparse.ArgumentParser(description=_UNSUPPORTED_MESSAGE)
94
+ # Accept old options only far enough to provide the quarantine diagnostic.
95
+ # They never select inputs, create a destination, or execute the old workflow.
96
+ parser.parse_known_args()
97
+ parser.error(_UNSUPPORTED_MESSAGE)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
equiframe/bundle.py ADDED
@@ -0,0 +1,240 @@
1
+ """Closed portable evidence, retaining historical bytes and resolving owned artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Literal
9
+
10
+ from pydantic import model_validator
11
+ from typing_extensions import Self
12
+
13
+ from equiframe._base import FrozenModel, Text
14
+ from equiframe._regular_io import (
15
+ new_directory_path,
16
+ open_regular_file,
17
+ regular_directory,
18
+ regular_files,
19
+ )
20
+ from equiframe.errors import ArtifactIntegrityError
21
+ from equiframe.evidence import (
22
+ MAX_DOCUMENT_BYTES,
23
+ describe_file,
24
+ parse_json,
25
+ publish_json,
26
+ verify_files,
27
+ )
28
+ from equiframe.local import _bundle_paths, local_path
29
+ from equiframe.pipeline_types import EvidenceFile
30
+ from equiframe.types import ArtifactRef
31
+
32
+
33
+ class BundleReference(FrozenModel):
34
+ uri: Text
35
+ member: EvidenceFile
36
+
37
+
38
+ class EvidenceBundle(FrozenModel):
39
+ schema_version: Literal["1.0"] = "1.0"
40
+ kind: Literal["migration", "verification", "workflow"]
41
+ root_document: Text
42
+ files: tuple[EvidenceFile, ...]
43
+ references: tuple[BundleReference, ...]
44
+ producer: Literal["equiframe"] = "equiframe"
45
+ producer_version: Text
46
+
47
+ @model_validator(mode="after")
48
+ def consistent(self) -> Self:
49
+ names = {f.path for f in self.files}
50
+ if len(names) != len(self.files) or self.root_document not in names:
51
+ raise ValueError("bundle requires a unique closed inventory and root document")
52
+ if "bundle.json" in names:
53
+ raise ValueError("bundle cannot inventory its own root manifest")
54
+ if len({r.uri for r in self.references}) != len(self.references):
55
+ raise ValueError("bundle URI mappings must be unique")
56
+ inventory = {f.path: f for f in self.files}
57
+ if any(inventory.get(r.member.path) != r.member for r in self.references):
58
+ raise ValueError("bundle mapping must bind an inventoried member")
59
+ return self
60
+
61
+
62
+ def _members(root: Path) -> tuple[Path, ...]:
63
+ return regular_files(root)
64
+
65
+
66
+ def _references(value: object) -> tuple[ArtifactRef, ...]:
67
+ result: list[ArtifactRef] = []
68
+ if isinstance(value, dict):
69
+ if {"uri", "sha256", "format", "media_type", "size_bytes"} <= value.keys():
70
+ result.append(ArtifactRef.model_validate(value))
71
+ else:
72
+ for child in value.values():
73
+ result.extend(_references(child))
74
+ elif isinstance(value, list):
75
+ for child in value:
76
+ result.extend(_references(child))
77
+ return tuple(result)
78
+
79
+
80
+ def _document_references(root: Path, members: tuple[Path, ...]) -> dict[str, ArtifactRef]:
81
+ result: dict[str, ArtifactRef] = {}
82
+ for path in members:
83
+ if path.suffix == ".json":
84
+ with open_regular_file(path) as stream:
85
+ raw = stream.read(MAX_DOCUMENT_BYTES + 1)
86
+ if len(raw) > MAX_DOCUMENT_BYTES:
87
+ raise ArtifactIntegrityError("portable document exceeds its byte budget")
88
+ for ref in _references(parse_json(raw)):
89
+ if ref.uri in result and result[ref.uri] != ref:
90
+ raise ArtifactIntegrityError("one artifact URI has conflicting identities")
91
+ result[ref.uri] = ref
92
+ return result
93
+
94
+
95
+ def _inspect(root: Path, kind: str, document: str) -> object:
96
+ if kind == "migration":
97
+ from equiframe.migration.io import read_migration
98
+
99
+ return read_migration(root / document)
100
+ if kind == "verification":
101
+ from equiframe.verify import read_verification
102
+
103
+ return read_verification(root / document)
104
+ from equiframe.workflow import read_workflow
105
+
106
+ return read_workflow(root / document)
107
+
108
+
109
+ def export_bundle(source: Path, destination: Path) -> EvidenceBundle:
110
+ """Export checked migration/Verify/workflow bytes to a new directory.
111
+
112
+ No artifact is fetched from outside the source. External provenance can map
113
+ only to an already captured member with exactly the same hash and size.
114
+ """
115
+ if source.is_file():
116
+ with open_regular_file(source):
117
+ pass
118
+ source = source.parent
119
+ source = regular_directory(source).resolve()
120
+ names = {
121
+ "migration.json": "migration",
122
+ "verification.json": "verification",
123
+ "workflow.json": "workflow",
124
+ }
125
+ found = [(name, kind) for name, kind in names.items() if (source / name).is_file()]
126
+ if len(found) != 1:
127
+ raise ValueError("export requires one supported finalized operation")
128
+ document, kind = found[0]
129
+ members = _members(source)
130
+ _inspect(source, kind, document)
131
+ refs = _document_references(source, members)
132
+ entries = tuple(describe_file(p, source) for p in members)
133
+ by_path = {f.path: f for f in entries}
134
+ by_identity = {(f.sha256, f.size_bytes): f for f in entries}
135
+ mappings: list[BundleReference] = []
136
+ for ref in refs.values():
137
+ p = local_path(ref).resolve()
138
+ item = by_path.get(p.relative_to(source).as_posix()) if p.is_relative_to(source) else None
139
+ if item is None:
140
+ item = by_identity.get((ref.sha256, ref.size_bytes))
141
+ if item is None or (item.sha256, item.size_bytes) != (ref.sha256, ref.size_bytes):
142
+ raise ArtifactIntegrityError("artifact reference is outside the captured bundle")
143
+ member = EvidenceFile(
144
+ path="operation/" + item.path, sha256=item.sha256, size_bytes=item.size_bytes
145
+ )
146
+ mappings.append(BundleReference(uri=ref.uri, member=member))
147
+ destination = new_directory_path(destination)
148
+ if destination.is_relative_to(source):
149
+ raise ValueError("portable destination must be outside the source operation")
150
+ destination.mkdir(parents=True, exist_ok=False)
151
+ shutil.copytree(source, destination / "operation")
152
+ from equiframe import __version__
153
+
154
+ bundle = EvidenceBundle.model_validate(
155
+ dict(
156
+ kind=kind,
157
+ root_document="operation/" + document,
158
+ files=tuple(
159
+ EvidenceFile(path="operation/" + e.path, sha256=e.sha256, size_bytes=e.size_bytes)
160
+ for e in entries
161
+ ),
162
+ references=tuple(mappings),
163
+ producer_version=__version__,
164
+ )
165
+ )
166
+ publish_json(bundle, destination / "bundle.json")
167
+ read_bundle(destination)
168
+ return bundle
169
+
170
+
171
+ def read_bundle(path: Path, *, expected_sha256: str | None = None) -> EvidenceBundle:
172
+ """Verify membership and operation; only None omits the optional manifest pin."""
173
+ root = path if path.is_dir() else path.parent
174
+ root = regular_directory(root).resolve()
175
+ manifest = root / "bundle.json"
176
+ with open_regular_file(manifest) as stream:
177
+ raw = stream.read(MAX_DOCUMENT_BYTES + 1)
178
+ if len(raw) > MAX_DOCUMENT_BYTES:
179
+ raise ArtifactIntegrityError("portable manifest exceeds its byte budget")
180
+ if expected_sha256 is not None and hashlib.sha256(raw).hexdigest() != expected_sha256:
181
+ raise ArtifactIntegrityError("portable manifest differs from trusted SHA-256")
182
+ parse_json(raw)
183
+ bundle = EvidenceBundle.model_validate_json(raw)
184
+ members = _members(root)
185
+ actual = {p.relative_to(root).as_posix() for p in members if p != manifest}
186
+ if actual != {f.path for f in bundle.files}:
187
+ raise ArtifactIntegrityError("portable inventory is not closed")
188
+ verify_files(root, bundle.files)
189
+ mappings = {r.uri: root / r.member.path for r in bundle.references}
190
+ refs = _document_references(root, tuple(p for p in members if p != manifest))
191
+ if set(refs) != set(mappings):
192
+ raise ArtifactIntegrityError("portable URI mapping differs from captured documents")
193
+ for uri, ref in refs.items():
194
+ entry = next(r.member for r in bundle.references if r.uri == uri)
195
+ if (entry.sha256, entry.size_bytes) != (ref.sha256, ref.size_bytes):
196
+ raise ArtifactIntegrityError("portable reference content identity mismatch")
197
+ with _bundle_paths(mappings):
198
+ _inspect(root, bundle.kind, bundle.root_document)
199
+ return bundle
200
+
201
+
202
+ def import_bundle(
203
+ source: Path, destination: Path, *, expected_sha256: str | None = None
204
+ ) -> EvidenceBundle:
205
+ """Copy to a new directory and verify again; original bytes are preserved."""
206
+ read_bundle(source, expected_sha256=expected_sha256)
207
+ root = source if source.is_dir() else source.parent
208
+ destination = new_directory_path(destination)
209
+ if destination.exists() or destination.is_relative_to(root.resolve()):
210
+ raise ValueError("bundle import requires a new destination outside the source")
211
+ shutil.copytree(root, destination)
212
+ return read_bundle(destination, expected_sha256=expected_sha256)
213
+
214
+
215
+ def inspect_bundle(path: Path, *, expected_sha256: str | None = None) -> dict[str, object]:
216
+ """Inspect semantic/technical outcomes while resolving original portable URIs."""
217
+ bundle = read_bundle(path, expected_sha256=expected_sha256)
218
+ root = (path if path.is_dir() else path.parent).resolve()
219
+ mapping = {r.uri: root / r.member.path for r in bundle.references}
220
+ with _bundle_paths(mapping):
221
+ if bundle.kind == "migration":
222
+ from equiframe.migration.io import migration_summary, read_migration
223
+
224
+ outcome = migration_summary(read_migration(root / bundle.root_document))
225
+ elif bundle.kind == "verification":
226
+ from equiframe.verify import read_verification, verification_summary
227
+
228
+ outcome = verification_summary(read_verification(root / bundle.root_document))
229
+ else:
230
+ from equiframe.workflow import read_workflow
231
+
232
+ workflow = read_workflow(root / bundle.root_document)
233
+ outcome = {
234
+ "run_id": workflow.run_id,
235
+ "complete": workflow.complete,
236
+ "exit_code": workflow.exit_code,
237
+ "first_divergent_stage": workflow.first_divergent_stage,
238
+ "checkpoints": [c.model_dump(mode="json") for c in workflow.checkpoints],
239
+ }
240
+ return {"kind": bundle.kind, "integrity": "passed", "outcome": outcome}