equiframe 0.12.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- equiframe/__init__.py +832 -0
- equiframe/__main__.py +3 -0
- equiframe/_base.py +84 -0
- equiframe/_cli_support.py +125 -0
- equiframe/_comparison_io.py +161 -0
- equiframe/_dataset_provenance.py +146 -0
- equiframe/_decimal.py +22 -0
- equiframe/_diff_capture.py +433 -0
- equiframe/_doctor.py +123 -0
- equiframe/_document_io.py +61 -0
- equiframe/_documents.py +79 -0
- equiframe/_duckdb_transport.py +149 -0
- equiframe/_error_reporting.py +52 -0
- equiframe/_first_use.py +102 -0
- equiframe/_path_compat.py +26 -0
- equiframe/_physical.py +133 -0
- equiframe/_r_review.R +308 -0
- equiframe/_regular_io.py +167 -0
- equiframe/_report_format_v1.py +298 -0
- equiframe/_report_io.py +591 -0
- equiframe/_stability_inventory.py +1421 -0
- equiframe/_timezones.py +257 -0
- equiframe/_vendor/cyclonedx/LICENSE +201 -0
- equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
- equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
- equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
- equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
- equiframe/adapters.py +454 -0
- equiframe/assertion_types.py +122 -0
- equiframe/assertions.py +271 -0
- equiframe/benchmark.py +101 -0
- equiframe/bundle.py +240 -0
- equiframe/cli.py +693 -0
- equiframe/comparison.py +937 -0
- equiframe/comparison_report.py +660 -0
- equiframe/comparison_types.py +257 -0
- equiframe/compatibility.py +546 -0
- equiframe/contracts.py +879 -0
- equiframe/datasets.py +1301 -0
- equiframe/decision.py +34 -0
- equiframe/destinations.py +295 -0
- equiframe/detector_types.py +431 -0
- equiframe/detectors.py +389 -0
- equiframe/diff.py +192 -0
- equiframe/diff_contracts.py +523 -0
- equiframe/docs/adoption.md +329 -0
- equiframe/docs/api.md +534 -0
- equiframe/docs/arquitecto.md +143 -0
- equiframe/docs/choosing-api.md +84 -0
- equiframe/docs/cientifico.md +180 -0
- equiframe/docs/compatibility.md +254 -0
- equiframe/docs/contributing.md +171 -0
- equiframe/docs/dependencies.md +237 -0
- equiframe/docs/eda.md +166 -0
- equiframe/docs/extending.md +158 -0
- equiframe/docs/fast-ci.md +52 -0
- equiframe/docs/first-use.md +78 -0
- equiframe/docs/index.md +142 -0
- equiframe/docs/ingestion.md +145 -0
- equiframe/docs/inicio.md +129 -0
- equiframe/docs/installed-private.md +97 -0
- equiframe/docs/journeys.md +362 -0
- equiframe/docs/lectura-conversion.md +211 -0
- equiframe/docs/licensing.md +17 -0
- equiframe/docs/maintenance.md +233 -0
- equiframe/docs/migration-ir.md +333 -0
- equiframe/docs/option-audit.md +45 -0
- equiframe/docs/outcome-gates.md +134 -0
- equiframe/docs/performance.md +263 -0
- equiframe/docs/piloto-simulado.md +56 -0
- equiframe/docs/quickstart.md +156 -0
- equiframe/docs/r_review.md +212 -0
- equiframe/docs/recipes.md +248 -0
- equiframe/docs/releasing.md +183 -0
- equiframe/docs/security.md +218 -0
- equiframe/docs/semantics.md +274 -0
- equiframe/docs/sqlite-snapshots.md +262 -0
- equiframe/docs/universal-sources.md +49 -0
- equiframe/docs/universalidad-cinco-pasos.md +107 -0
- equiframe/docs/upgrading.md +538 -0
- equiframe/duckdb_capture.py +410 -0
- equiframe/eda/__init__.py +36 -0
- equiframe/eda/__main__.py +3 -0
- equiframe/eda/_backend.py +612 -0
- equiframe/eda/_inventory.py +49 -0
- equiframe/eda/_paths.py +49 -0
- equiframe/eda/_report.py +291 -0
- equiframe/eda/api.py +535 -0
- equiframe/eda/cli.py +102 -0
- equiframe/eda/models.py +239 -0
- equiframe/errors.py +83 -0
- equiframe/evidence.py +375 -0
- equiframe/examples/integrations/eda_generic.py +60 -0
- equiframe/examples/integrations/outcome_gates.py +94 -0
- equiframe/examples/integrations/sqlite_snapshot.py +246 -0
- equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
- equiframe/examples/integrations/universal_first_use.py +296 -0
- equiframe/examples/journeys/README.md +31 -0
- equiframe/examples/journeys/__init__.py +1 -0
- equiframe/examples/journeys/a_pandas_refactor.py +88 -0
- equiframe/examples/journeys/b_native_migration.py +181 -0
- equiframe/examples/journeys/c_retail_etl.py +460 -0
- equiframe/examples/journeys/common.py +136 -0
- equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
- equiframe/examples/journeys/e_telemetry.py +135 -0
- equiframe/examples/journeys/f_data_science_preparation.py +84 -0
- equiframe/examples/journeys/g_external_snapshots.py +197 -0
- equiframe/examples/journeys/h_portable_ci.py +129 -0
- equiframe/examples/journeys/requirements.lock +249 -0
- equiframe/examples/journeys/verify_journeys.py +117 -0
- equiframe/examples/load_convert_compare.py +85 -0
- equiframe/execution.py +355 -0
- equiframe/execution_types.py +177 -0
- equiframe/ingestion.py +744 -0
- equiframe/local.py +126 -0
- equiframe/migration/__init__.py +94 -0
- equiframe/migration/_physical.py +8 -0
- equiframe/migration/backends/_timestamp_transport.py +109 -0
- equiframe/migration/backends/duckdb.py +651 -0
- equiframe/migration/backends/duckdb_decimal.py +125 -0
- equiframe/migration/backends/duckdb_time.py +87 -0
- equiframe/migration/backends/pandas.py +544 -0
- equiframe/migration/backends/polars.py +915 -0
- equiframe/migration/backends/polars_decimal.py +231 -0
- equiframe/migration/io.py +198 -0
- equiframe/migration/ports.py +96 -0
- equiframe/migration/runner.py +545 -0
- equiframe/migration/runner_types.py +405 -0
- equiframe/migration/types.py +354 -0
- equiframe/migration/validation.py +619 -0
- equiframe/normalization.py +358 -0
- equiframe/pipeline.py +274 -0
- equiframe/pipeline_types.py +247 -0
- equiframe/ports.py +39 -0
- equiframe/preprocessing.py +110 -0
- equiframe/profile_types.py +343 -0
- equiframe/profiles.py +447 -0
- equiframe/py.typed +0 -0
- equiframe/r_review.py +706 -0
- equiframe/reduction.py +518 -0
- equiframe/registry.py +121 -0
- equiframe/report_rendering.py +366 -0
- equiframe/report_types.py +294 -0
- equiframe/reporting.py +26 -0
- equiframe/rule_types.py +178 -0
- equiframe/rules.py +819 -0
- equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
- equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
- equiframe/schemas/r-review-options-1.0.schema.json +38 -0
- equiframe/schemas/r-review-result-1.0.schema.json +207 -0
- equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
- equiframe/serialization.py +152 -0
- equiframe/snapshot.py +296 -0
- equiframe/sources.py +283 -0
- equiframe/tables.py +459 -0
- equiframe/types.py +71 -0
- equiframe/values.py +252 -0
- equiframe/verification_request.py +35 -0
- equiframe/verify.py +799 -0
- equiframe/verify_types.py +255 -0
- equiframe/workflow.py +510 -0
- equiframe/workflow_request.py +38 -0
- equiframe-0.12.4.dist-info/METADATA +268 -0
- equiframe-0.12.4.dist-info/RECORD +170 -0
- equiframe-0.12.4.dist-info/WHEEL +4 -0
- equiframe-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
equiframe/assertions.py
ADDED
|
@@ -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}
|