fix(campaign): suppress overlapping aggregate cells

This commit is contained in:
2026-07-22 09:17:00 +02:00
parent ac3329cafe
commit 1225802c5d
2 changed files with 47 additions and 4 deletions

View File

@@ -268,15 +268,24 @@ def _build_aggregate_campaign_report(
outcome_values,
threshold=policy.small_cell_threshold,
)
outcome_suppression_applied = denominator.suppressed or any(
item.suppressed for item in outcomes.values()
)
inactive_entries = _inactive_entry_count(version)
standalone_counts = {
"inactive_source_entries": _suppress_standalone_count(
inactive_entries,
threshold=policy.small_cell_threshold,
),
"excluded_or_blocked_jobs": _suppress_standalone_count(
# This population count overlaps the outcome partition, so exposing it
# can make a suppressed outcome recoverable through subtraction.
"excluded_or_blocked_jobs": (
AggregateCount(value=None, suppressed=True)
if outcome_suppression_applied
else _suppress_standalone_count(
facts.excluded_or_blocked,
threshold=policy.small_cell_threshold,
)
),
}
suppression_applied = denominator.suppressed or any(
@@ -322,7 +331,8 @@ def _build_aggregate_campaign_report(
suppression_applied=suppression_applied,
rule=(
"Positive counts below the threshold are hidden. At least one additional "
"count or the denominator is hidden when needed to prevent subtraction."
"count or the denominator is hidden when needed to prevent subtraction. "
"Overlapping population counts are hidden whenever outcome suppression applies."
),
),
)

View File

@@ -141,6 +141,39 @@ def test_small_cells_use_primary_and_complementary_suppression() -> None:
assert report.privacy.suppression_applied is True
def test_outcome_suppression_also_hides_overlapping_population_count() -> None:
jobs = [
*[
_job(
index,
"smtp_accepted",
validation_status="blocked" if index < 4 else "ready",
)
for index in range(8)
],
_job(8, "skipped", validation_status="excluded"),
]
report = build_aggregate_campaign_report(
campaign=_campaign(), # type: ignore[arg-type]
version=_version(inactive_count=5), # type: ignore[arg-type]
jobs=jobs, # type: ignore[arg-type]
policy=_policy(5),
)
assert report.outcomes.excluded.suppressed is True
assert report.population.excluded_or_blocked_jobs.model_dump() == {
"value": None,
"suppressed": True,
}
# Source entries without jobs are outside the outcome denominator and do
# not overlap the suppressed partition, so their threshold-safe count stays visible.
assert report.population.inactive_source_entries.model_dump() == {
"value": 5,
"suppressed": False,
}
def test_all_small_cells_also_suppress_the_denominator_and_state() -> None:
jobs = [_job(0, "smtp_accepted"), _job(1, "failed_permanent")]