From a823342b5bccf816008171562e6c3efc6a0011bf Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 19 Jul 2026 01:52:01 +0200 Subject: [PATCH] Scope bounded coverage to persisted extents --- CHANGELOG.md | 2 + .../app/services/coverage_registry_service.py | 22 +++++++++- backend/tests/test_rc4_national_coverage.py | 41 +++++++++++++++++++ docs/CODEX_EXECUTION_LOG.md | 3 ++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1984222..ddc6b2d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ catalog content hash remain mandatory stable identity fields. - Kept Statbel population materialization theme-specific so it can never be reported as an NGI administrative boundary layer. +- Made bounded API coverage spatially honest: a persisted provider selection + is operational only for selections contained by its retained source bbox. ## Autonomous Belgium and North Sea RC program (2026-07-17) diff --git a/backend/app/services/coverage_registry_service.py b/backend/app/services/coverage_registry_service.py index 1f8a9080..a63cc5d1 100644 --- a/backend/app/services/coverage_registry_service.py +++ b/backend/app/services/coverage_registry_service.py @@ -422,6 +422,7 @@ class CoverageRegistryService: definition: _SourceDefinition, theme: str, zone: str, + selection: Any, ) -> list[Dataset]: if definition.operational_themes and theme not in definition.operational_themes: return [] @@ -444,6 +445,18 @@ class CoverageRegistryService: coverage_zones = metadata.get("coverage_zones") or metadata.get("coverage_zone") or [] if isinstance(coverage_zones, str): coverage_zones = [coverage_zones] + acquired_bbox = metadata.get("bbox_epsg4326") + if ( + definition.contract.acquisition_mode == "bounded_api" + and isinstance(acquired_bbox, list) + and len(acquired_bbox) == 4 + ): + try: + acquired_scope = box(*(float(value) for value in acquired_bbox)) + except (TypeError, ValueError): + continue + if not acquired_scope.is_valid or not acquired_scope.covers(selection): + continue layer_matches = not layer_names or dataset.reference_layer_name in layer_names zone_matches = not coverage_zones or zone in coverage_zones or "belgium" in coverage_zones if layer_matches and zone_matches: @@ -456,6 +469,7 @@ class CoverageRegistryService: zone: str, theme: str, datasets: list[Dataset], + selection: Any, ) -> CoverageResolutionItem: definitions = [ definition @@ -481,6 +495,7 @@ class CoverageRegistryService: definition, theme, zone, + selection, ) materialized.extend(matches) if matches: @@ -535,7 +550,12 @@ class CoverageRegistryService: ) items = [ - CoverageRegistryService._resolve_item(zone=zone, theme=theme, datasets=datasets) + CoverageRegistryService._resolve_item( + zone=zone, + theme=theme, + datasets=datasets, + selection=selection, + ) for zone in zones for theme in requested_themes ] diff --git a/backend/tests/test_rc4_national_coverage.py b/backend/tests/test_rc4_national_coverage.py index b9e1f8eb..e6342882 100644 --- a/backend/tests/test_rc4_national_coverage.py +++ b/backend/tests/test_rc4_national_coverage.py @@ -180,6 +180,47 @@ def test_statbel_population_materialization_does_not_masquerade_as_admin_data() assert population.materialized_dataset_ids == [statbel_id] +def test_bounded_api_materialization_only_covers_its_persisted_bbox() -> None: + project_id = uuid4() + dataset_id = uuid4() + dataset = SimpleNamespace( + id=dataset_id, + status="ready", + source_name="spw_picc", + reference_layer_name="buildings", + source_metadata={ + "coverage_zones": ["wallonia"], + "bbox_epsg4326": [4.55, 50.58, 4.56, 50.59], + }, + ) + session = FakeSession( + project=SimpleNamespace(id=project_id), + areas=[ + scope_area("Belgium land", box(2.5, 49.5, 6.4, 51.5)), + scope_area("Wallonia", box(2.5, 49.5, 6.4, 50.8)), + ], + datasets=[dataset], + ) + + inside = CoverageRegistryService.resolve( + session, + project_id, + CoverageBBox(minx=4.551, miny=50.581, maxx=4.559, maxy=50.589), + ["buildings"], + ) + outside = CoverageRegistryService.resolve( + session, + project_id, + CoverageBBox(minx=4.7, miny=50.6, maxx=4.71, maxy=50.61), + ["buildings"], + ) + + assert inside.items[0].status == "operational" + assert inside.items[0].materialized_dataset_ids == [dataset_id] + assert outside.items[0].status == "partial" + assert outside.items[0].materialized_dataset_ids == [] + + def test_mixed_land_and_north_sea_selection_remains_split() -> None: project_id = uuid4() project = SimpleNamespace(id=project_id) diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index cd52a695..968790d7 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -28,6 +28,9 @@ the Statbel population Dataset also appeared under `admin`. The coverage registry now enforces each source's `operational_themes`, leaving NGI as the administrative authority and Statbel as population evidence only. +- Bounded provider materialization now also checks its persisted + `bbox_epsg4326`; one small PICC/UrbIS/GRB/raster acquisition can no longer + claim that an entire region is locally loaded. - Focused backend and frontend suites passed before full release validation; full local and Tower evidence follows in the final P5 gate.