Enable governed GRB evolution
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-16 19:30:46 +02:00
parent 1d515e4901
commit a8b18942f5
12 changed files with 448 additions and 59 deletions
@@ -6,6 +6,8 @@ from types import SimpleNamespace
from uuid import uuid4
import pytest
from geoalchemy2.shape import from_shape
from shapely.geometry import Polygon
from app.core.errors import AppError
from app.models import Dataset, DatasetVersion
@@ -46,6 +48,33 @@ class SequenceScalarSession:
return ScalarQuery(next(self.values))
class FeatureRowsQuery:
def __init__(self, rows: list[object]):
self.rows = rows
self.row_limit: int | None = None
def filter(self, *args): # noqa: ANN002, ARG002
return self
def order_by(self, *args): # noqa: ANN002, ARG002
return self
def limit(self, value: int):
self.row_limit = value
return self
def all(self):
return self.rows[: self.row_limit]
class SequentialFeatureSession:
def __init__(self, row_sets: list[list[object]]):
self.row_sets = iter(row_sets)
def query(self, _model):
return FeatureRowsQuery(next(self.row_sets))
class VersionQuery:
def __init__(self, latest: DatasetVersion | None):
self.latest = latest
@@ -104,6 +133,40 @@ def temporal_dataset(*, project_id, observed_year: int, metric_method: str = "fe
)
def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
dataset = temporal_dataset(project_id=project_id, observed_year=2026, metric_method="intersection_area")
dataset.observed_at = datetime(2026, 7, observed_day, tzinfo=timezone.utc)
dataset.source_version = f"2026-07-{observed_day:02d}"
dataset.source_name = "grb"
dataset.reference_layer_name = "buildings"
dataset.temporal_series_key = "grb:buildings:kempen-transport-region"
dataset.source_metadata = {
"authority_level": "authoritative",
"geometry_clipped_to_area": True,
"collection": "GRB/GBG",
"selection_aggregation": {
"method": "intersection_area",
"label": "Bebouwde grondoppervlakte",
"unit": "ha",
},
}
dataset.provenance_metadata = {
"operator_tool": "provision_regional_grb_buildings.py",
"reference_truncated": False,
}
return dataset
def persisted_feature(dataset_id, source_feature_id: str | None, polygon: Polygon):
return SimpleNamespace(
id=uuid4(),
dataset_id=dataset_id,
source_feature_id=source_feature_id,
properties_json={},
geometry=from_shape(polygon, srid=4326),
)
def test_temporal_migration_and_models_align() -> None:
migration = (ROOT / "backend/alembic/versions/202607140001_temporal_dataset_foundation.py").read_text(encoding="utf-8")
for field in (
@@ -294,6 +357,63 @@ def test_unstable_temporal_identity_returns_clear_end_user_warning() -> None:
assert warnings == ["Wijzigingen van individuele objecten kunnen voor deze bron niet betrouwbaar worden gevolgd."]
def test_governed_legacy_grb_snapshots_use_verified_official_feature_identity() -> None:
project_id = uuid4()
earlier = governed_grb_dataset(project_id=project_id, observed_day=14)
later = governed_grb_dataset(project_id=project_id, observed_day=15)
original = Polygon([(5.1, 51.1), (5.101, 51.1), (5.101, 51.101), (5.1, 51.101)])
changed = Polygon([(5.1, 51.1), (5.102, 51.1), (5.102, 51.101), (5.1, 51.101)])
added = Polygon([(5.11, 51.11), (5.111, 51.11), (5.111, 51.111), (5.11, 51.111)])
session = SequentialFeatureSession(
[
[persisted_feature(earlier.id, "GBG.1", original)],
[persisted_feature(later.id, "GBG.1", changed), persisted_feature(later.id, "GBG.2", added)],
]
)
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
session,
earlier=earlier,
later=later,
bbox={"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2},
preview_limit=100,
)
assert changes.available is True
assert changes.added_count == 1
assert changes.removed_count == 0
assert changes.modified_count == 1
assert changes.unchanged_count == 0
assert {feature["properties"]["change_type"] for feature in geojson["features"]} == {"added", "modified"}
assert warnings == []
def test_governed_grb_object_history_fails_closed_for_unverified_identity() -> None:
project_id = uuid4()
earlier = governed_grb_dataset(project_id=project_id, observed_day=14)
later = governed_grb_dataset(project_id=project_id, observed_day=15)
polygon = Polygon([(5.1, 51.1), (5.101, 51.1), (5.101, 51.101), (5.1, 51.101)])
fallback_hash = "a" * 64
session = SequentialFeatureSession(
[
[persisted_feature(earlier.id, fallback_hash, polygon)],
[persisted_feature(later.id, fallback_hash, polygon)],
]
)
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
session,
earlier=earlier,
later=later,
bbox={"min_x": 5.0, "min_y": 51.0, "max_x": 5.2, "max_y": 51.2},
preview_limit=100,
)
assert changes.available is False
assert geojson["features"] == []
assert warnings == ["De geselecteerde objecten bevatten geen volledig verifieerbare stabiele bronidentiteit."]
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
@@ -304,6 +424,8 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
assert "Laatste toestand" in workspace
assert "Evolutie" in workspace
assert "Vergelijk periode" in workspace
assert "temporalRangeLabel" in workspace
assert "Dagelijkse GRB-edities tonen wijzigingen in de officiële registratie" in workspace
assert "/temporal/compare" in temporal_api
assert "Statbel" in population and "area_weighted_sum" in population
assert '"identity_stable": False' in population