Validate legacy GRB snapshot evidence
This commit is contained in:
@@ -500,7 +500,10 @@ class TemporalAnalysisService:
|
|||||||
dataset.source_name != "grb"
|
dataset.source_name != "grb"
|
||||||
or not str(dataset.temporal_series_key or "").startswith("grb:")
|
or not str(dataset.temporal_series_key or "").startswith("grb:")
|
||||||
or source_metadata.get("authority_level") != "authoritative"
|
or source_metadata.get("authority_level") != "authoritative"
|
||||||
or source_metadata.get("geometry_clipped_to_area") is not True
|
or not TemporalAnalysisService._has_governed_grb_area_contract(
|
||||||
|
source_metadata,
|
||||||
|
provenance,
|
||||||
|
)
|
||||||
or provenance.get("operator_tool") not in TemporalAnalysisService.GOVERNED_GRB_IDENTITY_OPERATORS
|
or provenance.get("operator_tool") not in TemporalAnalysisService.GOVERNED_GRB_IDENTITY_OPERATORS
|
||||||
or provenance.get("reference_truncated") is not False
|
or provenance.get("reference_truncated") is not False
|
||||||
):
|
):
|
||||||
@@ -515,6 +518,44 @@ class TemporalAnalysisService:
|
|||||||
prefixes = tuple(sorted(f"{str(collection)}:{str(collection)}." for collection in collections))
|
prefixes = tuple(sorted(f"{str(collection)}:{str(collection)}." for collection in collections))
|
||||||
return "grb_ogc_feature_id", prefixes
|
return "grb_ogc_feature_id", prefixes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_governed_grb_area_contract(
|
||||||
|
source_metadata: dict[str, Any],
|
||||||
|
provenance: dict[str, Any],
|
||||||
|
) -> bool:
|
||||||
|
if source_metadata.get("geometry_clipped_to_area") is True:
|
||||||
|
return True
|
||||||
|
|
||||||
|
partition_checksums = provenance.get("partition_checksums")
|
||||||
|
artifact_checksum = str(provenance.get("artifact_sha256") or "")
|
||||||
|
has_valid_checksum = len(artifact_checksum) == 64 and all(
|
||||||
|
character in "0123456789abcdefABCDEF" for character in artifact_checksum
|
||||||
|
)
|
||||||
|
has_valid_partition_checksums = (
|
||||||
|
isinstance(partition_checksums, dict)
|
||||||
|
and len(partition_checksums) == 28
|
||||||
|
and all(
|
||||||
|
len(str(checksum)) == 64
|
||||||
|
and all(character in "0123456789abcdefABCDEF" for character in str(checksum))
|
||||||
|
for checksum in partition_checksums.values()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
source_metadata.get("coverage_scope") == "kempen-transport-region"
|
||||||
|
and source_metadata.get("scope_type") == "transport_region"
|
||||||
|
and source_metadata.get("member_count") == 28
|
||||||
|
and source_metadata.get("partition_count") == 28
|
||||||
|
and source_metadata.get("partition_strategy")
|
||||||
|
in {
|
||||||
|
"municipality_bbox_maximum_boundary_intersection",
|
||||||
|
"municipality_bbox_maximum_same_dimension_intersection",
|
||||||
|
}
|
||||||
|
and bool(provenance.get("manifest_path"))
|
||||||
|
and bool(provenance.get("source_url") or provenance.get("source_urls"))
|
||||||
|
and has_valid_checksum
|
||||||
|
and has_valid_partition_checksums
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rows_match_identity_contract(rows: list[VectorFeature], prefixes: tuple[str, ...]) -> bool:
|
def _rows_match_identity_contract(rows: list[VectorFeature], prefixes: tuple[str, ...]) -> bool:
|
||||||
identities = [str(row.source_feature_id or "").strip() for row in rows]
|
identities = [str(row.source_feature_id or "").strip() for row in rows]
|
||||||
|
|||||||
@@ -142,8 +142,12 @@ def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
|
|||||||
dataset.temporal_series_key = "grb:buildings:kempen-transport-region"
|
dataset.temporal_series_key = "grb:buildings:kempen-transport-region"
|
||||||
dataset.source_metadata = {
|
dataset.source_metadata = {
|
||||||
"authority_level": "authoritative",
|
"authority_level": "authoritative",
|
||||||
"geometry_clipped_to_area": True,
|
|
||||||
"collection": "GRB/GBG",
|
"collection": "GRB/GBG",
|
||||||
|
"coverage_scope": "kempen-transport-region",
|
||||||
|
"scope_type": "transport_region",
|
||||||
|
"member_count": 28,
|
||||||
|
"partition_count": 28,
|
||||||
|
"partition_strategy": "municipality_bbox_maximum_boundary_intersection",
|
||||||
"selection_aggregation": {
|
"selection_aggregation": {
|
||||||
"method": "intersection_area",
|
"method": "intersection_area",
|
||||||
"label": "Bebouwde grondoppervlakte",
|
"label": "Bebouwde grondoppervlakte",
|
||||||
@@ -153,7 +157,13 @@ def governed_grb_dataset(*, project_id, observed_day: int) -> Dataset:
|
|||||||
dataset.provenance_metadata = {
|
dataset.provenance_metadata = {
|
||||||
"operator_tool": "provision_regional_grb_buildings.py",
|
"operator_tool": "provision_regional_grb_buildings.py",
|
||||||
"reference_truncated": False,
|
"reference_truncated": False,
|
||||||
|
"manifest_path": "/storage/operator/grb/manifest.json",
|
||||||
|
"source_url": "https://geo.api.vlaanderen.be/GRB/ogc/features/v1/collections/GBG/items",
|
||||||
|
"artifact_sha256": "a" * 64,
|
||||||
|
"partition_checksums": {f"{index:05d}": "b" * 64 for index in range(28)},
|
||||||
}
|
}
|
||||||
|
if observed_day > 14:
|
||||||
|
dataset.source_metadata["geometry_clipped_to_area"] = True
|
||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
@@ -414,6 +424,13 @@ def test_governed_grb_object_history_fails_closed_for_unverified_identity() -> N
|
|||||||
assert warnings == ["De geselecteerde objecten bevatten geen volledig verifieerbare stabiele bronidentiteit."]
|
assert warnings == ["De geselecteerde objecten bevatten geen volledig verifieerbare stabiele bronidentiteit."]
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_grb_identity_requires_complete_partition_evidence() -> None:
|
||||||
|
dataset = governed_grb_dataset(project_id=uuid4(), observed_day=14)
|
||||||
|
dataset.provenance_metadata["partition_checksums"] = {"13025": "b" * 64}
|
||||||
|
|
||||||
|
assert TemporalAnalysisService._identity_contract(dataset) is None
|
||||||
|
|
||||||
|
|
||||||
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
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")
|
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -9413,9 +9413,18 @@ Boundaries:
|
|||||||
to metric-only output whenever source identity cannot be proven.
|
to metric-only output whenever source identity cannot be proven.
|
||||||
|
|
||||||
Validation so far:
|
Validation so far:
|
||||||
- Focused GRB operator and temporal tests pass: 39 tests.
|
- Focused GRB operator and temporal tests pass: 39 tests in the first pass.
|
||||||
- Frontend typecheck and production build pass.
|
- The complete readiness gate passes after the legacy regression addition: 786
|
||||||
|
backend tests, 110 documented routes, one Alembic head, frontend typecheck,
|
||||||
|
production build and packaged script checks.
|
||||||
|
- The first Tower API comparison correctly exposed that the 14 July snapshots
|
||||||
|
predated the explicit `geometry_clipped_to_area` marker. Those snapshots are
|
||||||
|
now accepted only when their actual retained legacy evidence is complete:
|
||||||
|
the exact Kempen scope, 28 members and partitions, an approved assignment
|
||||||
|
strategy, manifest/source evidence, artifact checksum and 28 valid partition
|
||||||
|
checksums. A focused negative test proves incomplete partition evidence still
|
||||||
|
fails closed.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
- Run the full release gate, deploy, verify a real bounded GRB edition delta
|
- Redeploy the legacy-contract correction, verify a real bounded GRB edition
|
||||||
through the canonical API and inspect the daily Evolution UI on Tower.
|
delta through the canonical API and inspect the daily Evolution UI on Tower.
|
||||||
|
|||||||
Reference in New Issue
Block a user