Skip to content
2 changes: 2 additions & 0 deletions packages/populace-build/src/populace/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None:
export_surface_gate,
exported_nonzero_gate,
formula_owned_export_gate,
input_column_coverage_gate,
input_mass_parity_gate,
macro_realism_gate,
nonconstant_columns_gate,
Expand Down Expand Up @@ -155,6 +156,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None:
"export_surface_gate",
"exported_nonzero_gate",
"formula_owned_export_gate",
"input_column_coverage_gate",
"input_mass_parity_gate",
"load_ledger_consumer_artifact",
"macro_realism_gate",
Expand Down
128 changes: 128 additions & 0 deletions packages/populace-build/src/populace/build/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"export_surface_gate",
"formula_owned_export_gate",
"exported_nonzero_gate",
"input_column_coverage_gate",
"input_mass_parity_gate",
"macro_realism_gate",
"nonconstant_columns_gate",
Expand Down Expand Up @@ -1520,6 +1521,133 @@ def source_stage_input_coverage_gate(
)


def input_column_coverage_gate(
present_columns: Iterable[str],
*,
required_columns: Iterable[str],
degenerate_columns: Iterable[str] = (),
reviewed_exclusions: Mapping[str, str] | None = None,
name: str = "input_column_coverage",
) -> GateResult:
"""Every declared-required input column is present AND carries signal.

This is the release-blocking column-coverage contract of populace #368: a
release must persist every input column the reference eCPS exports, as a
real dataset key whose values are not all the engine default. It is the
generalization of ``assert_required_us_release_source_columns`` — which
guarded five SNAP/ACA/immigration columns — to the full eCPS input surface,
and it closes the gap the export-mass parity gate leaves open: parity only
covers the ~35 columns with a weighted-mass anchor, so an input the base
never imputed (SSI countable-resource assets, tips, overtime, education
credits, IRA/HSA — populace #340/#356/#361/#278) is *absent from the export
entirely* and every reform binding through it silently scores ~$0 while
every mass and parity gate still passes.

A required column fails when it is absent from ``present_columns`` (the
engine defaults it at simulation time) or listed in ``degenerate_columns``
(present as a key but every observed value equals the engine default, so
the export writer's default-broadcast makes it indistinguishable from
absence). Both are the same silent-zero failure and both are refused unless
the column carries a reviewed exclusion.

Reviewed exclusions accept a known, tracked gap by name with a reason (and,
by convention, the tracking issue in the reason). They carry the #286
"cannot rot" semantics: a reviewed exclusion whose column is now present
*and* non-degenerate is stale — the data caught up, so the column must be
promoted to a hard requirement — and fails the gate. An exclusion for a
column absent from this surface is dormant and only reported (release lines
persist different subsets). ``required_columns`` and
``reviewed_exclusions`` must be disjoint: a column is either a hard
requirement or a tracked gap, never both.

Args:
present_columns: Every input column the export persists as a key.
required_columns: Columns that must be present and non-degenerate.
degenerate_columns: The subset of present columns whose every observed
value equals the engine default (computed by the caller against the
engine's declared defaults). Columns not present are never here.
reviewed_exclusions: Column -> reason for tracked gaps allowed to be
absent or degenerate. Each needs a non-empty reason; a stale entry
fails the gate, a dormant entry is only reported.
name: Gate name for the manifest (defaults to
``"input_column_coverage"``).

Returns:
Pass iff every required column is present and non-degenerate, no
required column overlaps the exclusion register, and no exclusion is
stale.

Raises:
ValueError: If a reviewed exclusion has an empty reason, or a column is
both required and a reviewed exclusion.
TypeError: If ``reviewed_exclusions`` is not a mapping.
"""
if not name:
raise ValueError("input column coverage gate name must be non-empty.")
exclusions = _reviewed_exclusion_reasons(reviewed_exclusions)
present = {str(column) for column in present_columns}
degenerate = {str(column) for column in degenerate_columns} & present
required = {str(column) for column in required_columns}

both = sorted(required & set(exclusions))
if both:
raise ValueError(
"A column cannot be both a hard requirement and a reviewed "
f"exclusion: {both}."
)

failures: list[str] = []
missing: list[str] = []
degenerate_required: list[str] = []
for column in sorted(required):
if column not in present:
missing.append(column)
failures.append(
f"{column}: required eCPS input column is absent from the "
"export; the engine defaults it and every reform binding "
"through it scores ~$0. Impute it (carry it through the base "
"and selection) or record a reviewed exclusion with the "
"tracking issue."
)
elif column in degenerate:
degenerate_required.append(column)
failures.append(
f"{column}: required eCPS input column is present but every "
"value equals the engine default; the export writer's "
"default-broadcast makes it indistinguishable from absence. "
"Impute it or record a reviewed exclusion with the tracking "
"issue."
)

signal_present = present - degenerate
stale = sorted(column for column in exclusions if column in signal_present)
if stale:
failures.append(
"Stale reviewed exclusions — the column carries signal now, "
f"promote it to a hard requirement: {stale}."
)
dormant = sorted(column for column in exclusions if column not in present)

return GateResult(
name=name,
passed=not failures,
failures=tuple(failures),
details={
"required_columns": len(required),
"present_columns": len(present),
"missing": missing,
"degenerate_required": degenerate_required,
"reviewed_exclusions": {
column: reason
for column, reason in sorted(exclusions.items())
if column not in dormant
},
"stale_exclusions": stale,
"dormant_exclusions": dormant,
},
)


def parity_gate(
candidate_nonzero: Mapping[str, float],
reference_nonzero: Mapping[str, float],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"fiscal_target_references.json",
"obbba_reforms.json",
"puf_aggregate_record_disaggregation.json",
"release_input_coverage_manifest.json",
"soca_capital_gain_distribution_shares.json",
"soi_baseline_levels.json",
"source_stages.json",
Expand Down
Loading
Loading