From 076de860ed0b7ef781631f7d719b536fc29518fb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 3 Jun 2026 07:05:44 -0400 Subject: [PATCH] Add eCPS export support parity gate --- .../pipelines/check_export_columns.py | 299 +++++++++++++++++- .../pipelines/mp300k_artifact_gates.py | 78 +++++ tests/pipelines/test_check_export_columns.py | 198 ++++++++++++ tests/pipelines/test_mp300k_artifact_gates.py | 103 +++++- 4 files changed, 673 insertions(+), 5 deletions(-) diff --git a/src/microplex_us/pipelines/check_export_columns.py b/src/microplex_us/pipelines/check_export_columns.py index cbc6534..c8458d0 100644 --- a/src/microplex_us/pipelines/check_export_columns.py +++ b/src/microplex_us/pipelines/check_export_columns.py @@ -44,8 +44,9 @@ import argparse import json import sys -from dataclasses import dataclass +from dataclasses import asdict, dataclass from pathlib import Path +from typing import Any # Path to the committed contract shipped alongside this module. DEFAULT_CONTRACT_PATH = Path(__file__).with_name("ecps_export_contract.json") @@ -65,6 +66,43 @@ def ok(self) -> bool: return not self.missing_required and not self.forbidden_present +@dataclass +class ColumnSupportStats: + """Compact support/variation summary for one exported H5 column.""" + + column: str + kind: str + row_count: int + nonzero_count: int | None + unique_count: int + + +@dataclass +class ColumnSupportIssue: + """One eCPS-populated column missing equivalent MP support.""" + + column: str + requirement: str + baseline: ColumnSupportStats + candidate: ColumnSupportStats | None + + +@dataclass +class SupportDiff: + """Result of comparing candidate support against eCPS support.""" + + issues: list[ColumnSupportIssue] + checked_columns: list[str] + baseline_populated_columns: list[str] + baseline_filler_columns: list[str] + exempt_columns: list[str] + + @property + def ok(self) -> bool: + """True when every eCPS-populated column has candidate support.""" + return not self.issues + + def compute_column_diff( present: set[str], *, @@ -93,6 +131,171 @@ def compute_column_diff( ) +def compute_support_diff( + candidate_h5: Path, + *, + baseline_h5: Path, + period: int, + required_columns: set[str], + exempt_columns: frozenset[str] | set[str] = frozenset(), +) -> SupportDiff: + """Compare candidate support against eCPS support for required columns. + + Presence is not enough for release parity. If the pinned eCPS baseline + *populates* a required exported column, MP must populate it too: + + - numeric columns: eCPS has at least one nonzero value, so MP must also + have at least one nonzero value; + - boolean/string/categorical columns: eCPS has more than one unique value, + so MP must also vary. + + Columns where eCPS itself is all-zero/single-valued are treated as fillers + and do not require MP support. Explicit exemptions are reserved for known + rare, computed-downstream, or intentionally absent variables. + """ + period_key = str(int(period)) + exempt = {str(column) for column in exempt_columns} + checked_columns: list[str] = [] + baseline_populated_columns: list[str] = [] + baseline_filler_columns: list[str] = [] + issues: list[ColumnSupportIssue] = [] + + import h5py + + with ( + h5py.File(candidate_h5, "r") as candidate, + h5py.File(baseline_h5, "r") as baseline, + ): + for column in sorted(required_columns): + if column in exempt: + continue + baseline_values = _h5_column_values( + baseline, + column, + period_key=period_key, + ) + if baseline_values is None: + continue + checked_columns.append(column) + baseline_stats = _support_stats(column, baseline_values) + requirement = _support_requirement(baseline_stats) + if requirement is None: + baseline_filler_columns.append(column) + continue + baseline_populated_columns.append(column) + candidate_values = _h5_column_values( + candidate, + column, + period_key=period_key, + ) + candidate_stats = ( + None + if candidate_values is None + else _support_stats(column, candidate_values) + ) + if not _satisfies_support_requirement( + candidate_stats, + requirement=requirement, + ): + issues.append( + ColumnSupportIssue( + column=column, + requirement=requirement, + baseline=baseline_stats, + candidate=candidate_stats, + ) + ) + + return SupportDiff( + issues=issues, + checked_columns=checked_columns, + baseline_populated_columns=baseline_populated_columns, + baseline_filler_columns=baseline_filler_columns, + exempt_columns=sorted(exempt & set(required_columns)), + ) + + +def _h5_column_values( + handle: Any, + column: str, + *, + period_key: str, +): + """Return one H5 column's values, supporting grouped and flat layouts.""" + if column not in handle: + return None + item = handle[column] + import h5py + import numpy as np + + if isinstance(item, h5py.Group): + if period_key not in item: + return None + item = item[period_key] + if not isinstance(item, h5py.Dataset): + return None + return np.asarray(item) + + +def _support_stats(column: str, values) -> ColumnSupportStats: + """Summarize nonzero support and uniqueness for an exported column.""" + import numpy as np + + array = np.asarray(values) + flattened = array.reshape(-1) + unique_count = int(len(np.unique(flattened))) if flattened.size else 0 + kind = _support_kind(flattened) + nonzero_count: int | None = None + if kind == "numeric": + numeric = flattened + if np.issubdtype(numeric.dtype, np.floating): + numeric = numeric[np.isfinite(numeric)] + nonzero_count = int(np.count_nonzero(numeric)) + return ColumnSupportStats( + column=column, + kind=kind, + row_count=int(flattened.size), + nonzero_count=nonzero_count, + unique_count=unique_count, + ) + + +def _support_kind(values) -> str: + """Classify a NumPy array for support checking.""" + import numpy as np + + dtype = np.asarray(values).dtype + if np.issubdtype(dtype, np.bool_): + return "categorical" + if np.issubdtype(dtype, np.number): + return "numeric" + return "categorical" + + +def _support_requirement(stats: ColumnSupportStats) -> str | None: + """Return the support MP must match for an eCPS column, if any.""" + if stats.kind == "numeric": + return "numeric_nonzero" if (stats.nonzero_count or 0) > 0 else None + return "categorical_variation" if stats.unique_count > 1 else None + + +def _satisfies_support_requirement( + stats: ColumnSupportStats | None, + *, + requirement: str, +) -> bool: + """Return whether candidate stats meet an eCPS-derived requirement.""" + if stats is None: + return False + if requirement == "numeric_nonzero": + if stats.kind != "numeric": + return stats.unique_count > 1 + return (stats.nonzero_count or 0) > 0 + if requirement == "categorical_variation": + return stats.unique_count > 1 + raise ValueError(f"Unknown support requirement: {requirement}") + + def load_contract(path: Path) -> dict: """Load and validate the column-parity contract JSON.""" with open(path) as f: @@ -188,6 +391,7 @@ def _format_report( n_present: int, n_required: int, n_forbidden: int, + support_diff: SupportDiff | None = None, ) -> str: """Build a human-readable report for the diff.""" lines = [ @@ -203,12 +407,47 @@ def _format_report( *_bullet_lines(diff.forbidden_present), f" extra_unknown (informational, {len(diff.extra_unknown)}):", *_bullet_lines(diff.extra_unknown), - "", - " RESULT: " + ("PASS" if diff.ok else "FAIL"), ] + if support_diff is not None: + lines.extend( + [ + "", + " eCPS support parity:", + f" checked_columns: {len(support_diff.checked_columns)}", + f" eCPS-populated columns: {len(support_diff.baseline_populated_columns)}", + f" eCPS filler columns: {len(support_diff.baseline_filler_columns)}", + f" explicit support exemptions: {len(support_diff.exempt_columns)}", + f" unsupported_populated ({len(support_diff.issues)}):", + *_bullet_lines( + [ + f"{issue.column} ({issue.requirement}; " + f"eCPS={_compact_stats(issue.baseline)}, " + f"candidate={_compact_stats(issue.candidate)})" + for issue in support_diff.issues + ] + ), + ] + ) + ok = diff.ok and (support_diff is None or support_diff.ok) + lines.extend(["", " RESULT: " + ("PASS" if ok else "FAIL")]) return "\n".join(lines) +def _compact_stats(stats: ColumnSupportStats | None) -> str: + """Render support stats compactly for CLI output.""" + if stats is None: + return "missing" + if stats.kind == "numeric": + return f"nonzero {stats.nonzero_count}/{stats.row_count}" + return f"unique {stats.unique_count}/{stats.row_count}" + + +def support_diff_to_dict(diff: SupportDiff) -> dict[str, Any]: + """Return a JSON-serializable support parity payload.""" + payload = asdict(diff) + return payload + + def main(argv: list[str] | None = None) -> int: """Run the column-parity check; return the process exit code.""" parser = argparse.ArgumentParser( @@ -256,6 +495,37 @@ def main(argv: list[str] | None = None) -> int: default=str(DEFAULT_CONTRACT_PATH), help="Override the contract JSON (default: committed contract).", ) + parser.add_argument( + "--support-baseline", + metavar="H5", + help=( + "Pinned eCPS baseline H5. When supplied with an H5 candidate, " + "also fail if eCPS has nonzero/variant support for a required " + "exported column and the candidate is all-zero/constant." + ), + ) + parser.add_argument( + "--period", + type=int, + default=2024, + help="Tax year period to inspect for H5 support parity (default: 2024).", + ) + parser.add_argument( + "--support-exempt-column", + action="append", + default=[], + metavar="COLUMN", + help=( + "Required export column exempt from support parity because it is " + "declared rare, computed downstream, or intentionally absent. " + "Repeat for each explicit exception." + ), + ) + parser.add_argument( + "--support-diagnostics-json", + metavar="FILE", + help="Optional path to write support-parity diagnostics JSON.", + ) args = parser.parse_args(argv) selected_inputs = [ @@ -267,6 +537,8 @@ def main(argv: list[str] | None = None) -> int: parser.error( "provide exactly one of an H5 path, --columns-json, or --entity-tables." ) + if args.support_baseline and not args.h5path: + parser.error("--support-baseline requires an H5 candidate path.") contract = load_contract(Path(args.contract)) required = set(contract["required"]) @@ -294,6 +566,24 @@ def main(argv: list[str] | None = None) -> int: optional=optional, excluded=excluded, ) + support_diff = None + if args.support_baseline: + support_exempt = set(contract.get("support_exemptions", [])) | set( + args.support_exempt_column + ) + support_diff = compute_support_diff( + Path(args.h5path), + baseline_h5=Path(args.support_baseline), + period=int(args.period), + required_columns=required, + exempt_columns=support_exempt, + ) + if args.support_diagnostics_json: + output_path = Path(args.support_diagnostics_json) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + json.dumps(support_diff_to_dict(support_diff), indent=2) + "\n" + ) print( _format_report( diff, @@ -301,9 +591,10 @@ def main(argv: list[str] | None = None) -> int: n_present=len(present), n_required=len(required), n_forbidden=len(forbidden), + support_diff=support_diff, ) ) - return 0 if diff.ok else 1 + return 0 if diff.ok and (support_diff is None or support_diff.ok) else 1 if __name__ == "__main__": diff --git a/src/microplex_us/pipelines/mp300k_artifact_gates.py b/src/microplex_us/pipelines/mp300k_artifact_gates.py index 6e9c7c3..c8cbffd 100644 --- a/src/microplex_us/pipelines/mp300k_artifact_gates.py +++ b/src/microplex_us/pipelines/mp300k_artifact_gates.py @@ -48,6 +48,7 @@ "candidate_artifact", "compatibility", "column_contract", + "export_support", "artifact_size", "runtime", "source_weight_diagnostics", @@ -212,6 +213,11 @@ def build_mp300k_artifact_gate_report( baseline_dataset=baseline_dataset, period=period, ) + export_support_gate = _export_support_gate( + candidate_dataset, + baseline_dataset=baseline_dataset, + period=period, + ) artifact_size_gate = _artifact_size_gate( candidate_dataset, baseline_dataset=baseline_dataset, @@ -253,6 +259,7 @@ def build_mp300k_artifact_gate_report( "candidate_artifact": candidate_gate, "compatibility": compatibility_gate, "column_contract": column_contract_gate, + "export_support": export_support_gate, "artifact_size": artifact_size_gate, "runtime": runtime_gate, "source_weight_diagnostics": source_weight_diagnostics_gate, @@ -516,6 +523,77 @@ def _column_contract_gate( ) +def _export_support_gate( + candidate_dataset: Path, + *, + baseline_dataset: Path | None, + period: int, +) -> dict[str, Any]: + if baseline_dataset is None: + return _gate( + "unmeasured", + "pinned eCPS baseline H5 has not been attached for export-support comparison", + ) + if not candidate_dataset.exists() or not baseline_dataset.exists(): + missing = [ + str(path) + for path in (candidate_dataset, baseline_dataset) + if not path.exists() + ] + return _gate( + "fail", + "export-support comparison files are missing", + details={"missing": missing}, + ) + + period_key = str(int(period)) + baseline_columns = _h5_period_columns(baseline_dataset, period_key=period_key) + excluded_baseline_computed_columns = sorted( + _computed_policyengine_us_export_columns(baseline_columns) + ) + required_columns = set(baseline_columns) - set(excluded_baseline_computed_columns) + from microplex_us.pipelines.check_export_columns import ( + compute_support_diff, + support_diff_to_dict, + ) + + support_diff = compute_support_diff( + candidate_dataset, + baseline_h5=baseline_dataset, + period=period, + required_columns=required_columns, + ) + metrics = { + "period": int(period), + "checked_export_column_count": len(support_diff.checked_columns), + "ecps_populated_export_column_count": len( + support_diff.baseline_populated_columns + ), + "ecps_filler_export_column_count": len(support_diff.baseline_filler_columns), + "unsupported_populated_export_column_count": len(support_diff.issues), + "excluded_baseline_computed_column_count": len( + excluded_baseline_computed_columns + ), + } + details = { + **support_diff_to_dict(support_diff), + "excluded_baseline_computed_columns": excluded_baseline_computed_columns, + } + if support_diff.issues: + return _gate( + "fail", + "candidate export columns lack support for eCPS-populated columns", + metrics=metrics, + details=details, + ) + return _gate( + "pass", + "candidate export columns have support for every eCPS-populated export", + metrics=metrics, + details=details, + ) + + def _computed_policyengine_us_export_columns(columns: list[str]) -> set[str]: try: import policyengine_us diff --git a/tests/pipelines/test_check_export_columns.py b/tests/pipelines/test_check_export_columns.py index 34fb68a..c74162f 100644 --- a/tests/pipelines/test_check_export_columns.py +++ b/tests/pipelines/test_check_export_columns.py @@ -69,6 +69,16 @@ def _run_columns( ) +def _write_period_h5(path: Path, columns: dict[str, list[object]]) -> Path: + h5py = pytest.importorskip("h5py") + import numpy as np + + with h5py.File(path, "w") as f: + for column, values in columns.items(): + f.create_dataset(f"{column}/2024", data=np.asarray(values)) + return path + + def test_main_clean_list_returns_zero(tmp_path, contract_path): # required + optional, no forbidden -> pass. cols = ["age", "snap", "employment_income", "person_is_puf_clone"] @@ -150,6 +160,181 @@ def test_main_h5_path_accepts_flat_datasets(tmp_path, contract_path): assert rc == 0 +def test_support_baseline_rejects_numeric_column_eCPS_populates( + tmp_path, + contract_path, +): + candidate = _write_period_h5( + tmp_path / "candidate.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 0.0, 0.0], + }, + ) + baseline = _write_period_h5( + tmp_path / "baseline.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + + rc = main( + [ + str(candidate), + "--contract", + str(contract_path), + "--support-baseline", + str(baseline), + ] + ) + + assert rc == 1 + + +def test_support_baseline_rejects_categorical_column_eCPS_varies( + tmp_path, + contract_path, +): + candidate = _write_period_h5( + tmp_path / "candidate.h5", + { + "age": [34, 42, 50], + "snap": [False, False, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + baseline = _write_period_h5( + tmp_path / "baseline.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + + rc = main( + [ + str(candidate), + "--contract", + str(contract_path), + "--support-baseline", + str(baseline), + ] + ) + + assert rc == 1 + + +def test_support_baseline_ignores_ecps_filler_columns(tmp_path, contract_path): + candidate = _write_period_h5( + tmp_path / "candidate.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 0.0, 0.0], + }, + ) + baseline = _write_period_h5( + tmp_path / "baseline.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 0.0, 0.0], + }, + ) + + rc = main( + [ + str(candidate), + "--contract", + str(contract_path), + "--support-baseline", + str(baseline), + ] + ) + + assert rc == 0 + + +def test_support_baseline_accepts_candidate_categorical_support_for_numeric_ecps( + tmp_path, + contract_path, +): + candidate = _write_period_h5( + tmp_path / "candidate.h5", + { + "age": [b"34", b"42", b"50"], + "snap": [False, True, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + baseline = _write_period_h5( + tmp_path / "baseline.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + + rc = main( + [ + str(candidate), + "--contract", + str(contract_path), + "--support-baseline", + str(baseline), + ] + ) + + assert rc == 0 + + +def test_support_baseline_writes_diagnostics_and_honors_explicit_exemption( + tmp_path, + contract_path, +): + candidate = _write_period_h5( + tmp_path / "candidate.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 0.0, 0.0], + }, + ) + baseline = _write_period_h5( + tmp_path / "baseline.h5", + { + "age": [34, 42, 50], + "snap": [False, True, False], + "employment_income": [0.0, 12_000.0, 0.0], + }, + ) + diagnostics = tmp_path / "support.json" + + rc = main( + [ + str(candidate), + "--contract", + str(contract_path), + "--support-baseline", + str(baseline), + "--support-exempt-column", + "employment_income", + "--support-diagnostics-json", + str(diagnostics), + ] + ) + + assert rc == 0 + payload = json.loads(diagnostics.read_text()) + assert payload["issues"] == [] + assert payload["exempt_columns"] == ["employment_income"] + + def test_main_entity_tables_path_uses_schema_columns( tmp_path, contract_path, monkeypatch ): @@ -196,6 +381,19 @@ def test_main_requires_exactly_one_input(tmp_path, contract_path): ) assert exc.value.code == 2 + with pytest.raises(SystemExit) as exc: + main( + [ + "--columns-json", + str(cols_path), + "--support-baseline", + str(tmp_path / "baseline.h5"), + "--contract", + str(contract_path), + ] + ) + assert exc.value.code == 2 + def test_compute_column_diff_categories(): diff = compute_column_diff( diff --git a/tests/pipelines/test_mp300k_artifact_gates.py b/tests/pipelines/test_mp300k_artifact_gates.py index dfbcaef..44035d0 100644 --- a/tests/pipelines/test_mp300k_artifact_gates.py +++ b/tests/pipelines/test_mp300k_artifact_gates.py @@ -246,6 +246,7 @@ def test_write_mp300k_artifact_gate_report_passes_with_all_evidence(tmp_path): assert record["gates"]["compatibility"]["metrics"]["household_count"] == 2 assert record["gates"]["compatibility"]["metrics"]["person_count"] == 3 assert record["gates"]["column_contract"]["status"] == "pass" + assert record["gates"]["export_support"]["status"] == "pass" assert record["gates"]["artifact_size"]["status"] == "pass" assert record["gates"]["ecps_comparison"]["status"] == "pass" assert record["gates"]["arch_target_coverage"]["status"] == "pass" @@ -329,6 +330,104 @@ def test_column_contract_gate_rejects_missing_ecps_contract_column(tmp_path): assert column_gate["details"]["missing_contract_columns"] == ["age"] +def test_export_support_gate_rejects_ecps_populated_numeric_filler(tmp_path): + artifact_dir = tmp_path / "artifact" + artifact_dir.mkdir() + candidate_dataset = _write_minimal_policyengine_dataset( + artifact_dir / "candidate.h5" + ) + _add_period_dataset(candidate_dataset, "hourly_wage", [0.0, 0.0, 0.0]) + baseline_dataset = _write_minimal_policyengine_dataset(tmp_path / "baseline.h5") + _add_period_dataset(baseline_dataset, "hourly_wage", [0.0, 25.0, 0.0]) + benchmark_manifest = tmp_path / "benchmark_manifest.json" + _write_benchmark_manifest(benchmark_manifest) + _write_artifact_manifest(artifact_dir, baseline_dataset=baseline_dataset) + + report_path = write_mp300k_artifact_gate_report( + artifact_dir, + ecps_comparison_payload=_sound_ecps_comparison_payload(), + arch_coverage_payload=_arch_coverage_payload(), + runtime_smoke_payload={"runtime_ratio": 1.0}, + benchmark_manifest_path=benchmark_manifest, + compute_native_scores=False, + update_manifest=False, + ) + + record = json.loads(report_path.read_text()) + support_gate = record["gates"]["export_support"] + + assert record["summary"]["status"] == "failed" + assert support_gate["status"] == "fail" + assert support_gate["metrics"]["unsupported_populated_export_column_count"] == 1 + assert support_gate["details"]["issues"][0]["column"] == "hourly_wage" + assert support_gate["details"]["issues"][0]["requirement"] == "numeric_nonzero" + + +def test_export_support_gate_rejects_ecps_varied_categorical_filler(tmp_path): + artifact_dir = tmp_path / "artifact" + artifact_dir.mkdir() + candidate_dataset = _write_minimal_policyengine_dataset( + artifact_dir / "candidate.h5" + ) + _add_period_dataset(candidate_dataset, "is_tipped_occupation", [False, False]) + baseline_dataset = _write_minimal_policyengine_dataset(tmp_path / "baseline.h5") + _add_period_dataset(baseline_dataset, "is_tipped_occupation", [False, True]) + benchmark_manifest = tmp_path / "benchmark_manifest.json" + _write_benchmark_manifest(benchmark_manifest) + _write_artifact_manifest(artifact_dir, baseline_dataset=baseline_dataset) + + report_path = write_mp300k_artifact_gate_report( + artifact_dir, + ecps_comparison_payload=_sound_ecps_comparison_payload(), + arch_coverage_payload=_arch_coverage_payload(), + runtime_smoke_payload={"runtime_ratio": 1.0}, + benchmark_manifest_path=benchmark_manifest, + compute_native_scores=False, + update_manifest=False, + ) + + record = json.loads(report_path.read_text()) + support_gate = record["gates"]["export_support"] + + assert record["summary"]["status"] == "failed" + assert support_gate["status"] == "fail" + assert support_gate["details"]["issues"][0]["column"] == "is_tipped_occupation" + assert ( + support_gate["details"]["issues"][0]["requirement"] == "categorical_variation" + ) + + +def test_export_support_gate_ignores_ecps_filler_columns(tmp_path): + artifact_dir = tmp_path / "artifact" + artifact_dir.mkdir() + candidate_dataset = _write_minimal_policyengine_dataset( + artifact_dir / "candidate.h5" + ) + _add_period_dataset(candidate_dataset, "second_home_mortgage_interest", [0.0, 0.0]) + baseline_dataset = _write_minimal_policyengine_dataset(tmp_path / "baseline.h5") + _add_period_dataset(baseline_dataset, "second_home_mortgage_interest", [0.0, 0.0]) + benchmark_manifest = tmp_path / "benchmark_manifest.json" + _write_benchmark_manifest(benchmark_manifest) + _write_artifact_manifest(artifact_dir, baseline_dataset=baseline_dataset) + + report_path = write_mp300k_artifact_gate_report( + artifact_dir, + ecps_comparison_payload=_sound_ecps_comparison_payload(), + arch_coverage_payload=_arch_coverage_payload(), + runtime_smoke_payload={"runtime_ratio": 1.0}, + benchmark_manifest_path=benchmark_manifest, + compute_native_scores=False, + update_manifest=False, + ) + + record = json.loads(report_path.read_text()) + support_gate = record["gates"]["export_support"] + + assert record["summary"]["status"] == "passed" + assert support_gate["status"] == "pass" + assert support_gate["metrics"]["ecps_filler_export_column_count"] == 1 + + def test_column_contract_gate_rejects_extra_candidate_columns(tmp_path): artifact_dir = tmp_path / "artifact" artifact_dir.mkdir() @@ -407,7 +506,9 @@ def test_column_contract_gate_excludes_computed_baseline_outputs( artifact_dir.mkdir() _write_minimal_policyengine_dataset(artifact_dir / "candidate.h5") baseline_dataset = _write_minimal_policyengine_dataset(tmp_path / "baseline.h5") - _add_period_dataset(baseline_dataset, "traditional_ira_contributions", [0.0, 0.0, 0.0]) + _add_period_dataset( + baseline_dataset, "traditional_ira_contributions", [0.0, 0.0, 0.0] + ) _add_period_dataset( baseline_dataset, "self_employed_pension_contribution_ald",