diff --git a/CHANGELOG.md b/CHANGELOG.md index 485f744a..382895a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ - Made staged, reviewed and applied release evidence write-once. An idempotent apply validates and reuses the existing evidence instead of changing its timestamp or authorization history. +- Made the read-only source-freshness audit prefer the governed official + `YYYY.NN` orthophoto edition over retained legacy `most_recent_at_*` labels, + while preserving the existing rolling-source review interval. ## Sprint 230 Governed orthophoto release preflight (2026-07-17) diff --git a/backend/app/services/source_freshness_service.py b/backend/app/services/source_freshness_service.py index c7a0a3ef..ab8dc7cb 100644 --- a/backend/app/services/source_freshness_service.py +++ b/backend/app/services/source_freshness_service.py @@ -4,6 +4,7 @@ from collections import defaultdict from dataclasses import dataclass from datetime import datetime, timedelta, timezone from pathlib import Path +import re from typing import Iterable from uuid import UUID @@ -51,6 +52,7 @@ SOURCE_POLICIES: dict[str, SourcePolicy] = { } DEFAULT_POLICY = SourcePolicy("Niet-geclassificeerde bron", "edition") +_ORTHOPHOTO_EDITION = re.compile(r"^20\d{2}\.\d{2}$") def _as_utc(value: datetime | None) -> datetime | None: @@ -81,10 +83,16 @@ def _latest_dataset(datasets: list[Dataset]) -> Dataset: ) -def _latest_source_version(policy: SourcePolicy, datasets: list[Dataset]) -> str | None: +def _latest_source_version(source_name: str, policy: SourcePolicy, datasets: list[Dataset]) -> str | None: versioned = [item for item in datasets if item.source_version] if not versioned: return None + if source_name == "digitaal_vlaanderen_orthophoto": + official_editions = [ + item for item in versioned if _ORTHOPHOTO_EDITION.fullmatch((item.source_version or "").strip()) + ] + if official_editions: + return _latest_dataset(official_editions).source_version if policy.refresh_policy == "rolling_snapshot": named_current = [ item @@ -258,7 +266,7 @@ class SourceFreshnessService: version_count=sum(len(versions_by_dataset.get(item.id, [])) for item in source_datasets), latest_imported_at=_latest_datetime(item.imported_at for item in source_datasets), latest_observed_at=_latest_datetime(item.observed_at for item in source_datasets), - latest_source_version=_latest_source_version(policy, source_datasets), + latest_source_version=_latest_source_version(source_name, policy, source_datasets), refresh_policy=policy.refresh_policy, review_interval_days=policy.review_interval_days, next_review_at=next_review_at, diff --git a/backend/tests/test_sprint221_source_freshness_audit.py b/backend/tests/test_sprint221_source_freshness_audit.py index cf628cbe..d5386083 100644 --- a/backend/tests/test_sprint221_source_freshness_audit.py +++ b/backend/tests/test_sprint221_source_freshness_audit.py @@ -157,6 +157,33 @@ def test_rolling_orthophoto_prefers_explicit_current_snapshot_over_historical_ob assert report.items[0].latest_source_version == "most_recent_at_2026-07-14" +def test_orthophoto_freshness_prefers_governed_official_edition_over_rolling_marker() -> None: + legacy = _dataset( + "digitaal_vlaanderen_orthophoto", + imported_at=NOW - timedelta(days=2), + observed_at=datetime(2026, 7, 15, tzinfo=timezone.utc), + source_version="most_recent_at_2026-07-15", + ) + official = _dataset( + "digitaal_vlaanderen_orthophoto", + imported_at=NOW - timedelta(days=1), + observed_at=datetime(2025, 4, 5, tzinfo=timezone.utc), + source_version="2025.04", + ) + + report = SourceFreshnessService.build_report( + legacy.project_id, + [legacy, official], + [_version(legacy), _version(official)], + now=NOW, + ) + + item = report.items[0] + assert item.latest_source_version == "2025.04" + assert item.refresh_policy == "rolling_snapshot" + assert item.review_interval_days == 180 + + def test_spatial_partitions_do_not_become_a_false_historical_series() -> None: first = _dataset( "dov_soil_map", diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 9a1be236..174e76ef 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -9791,13 +9791,49 @@ Implemented: the original checksum-bound evidence and never rewrite approval history. Validation so far: -- 38 focused Sprint 222/230/231 tests pass with deprecations treated as errors. - Coverage includes official-edition ordering, first-baseline authorization, - GetMap allowlisting/limits, RGB/CRS normalization, preview generation, - tampering, preflight drift, named review, loopback-only apply and complete - upload provenance. +- 48 focused Sprint 221/222/230/231 tests pass. The release-management suite + contains 11 direct plan/stage/review/apply tests. Coverage includes + official-edition ordering in both catalog and source-freshness views, + first-baseline authorization, GetMap allowlisting/limits, RGB/CRS + normalization, preview generation, tampering, preflight drift, named review, + loopback-only apply and complete upload provenance. +- Complete local readiness passed with 878 backend tests, 110 documented + routes, one Alembic head `202607160001`, frontend typecheck and production + build. Target compile and Ruff checks also passed. + +Live validation: +- Commits `06d05f0` and `0c2ebc1` were pushed, deployed through the repository + to Tower and validated against PostGIS 3.6. The all-in-one container became + healthy on port 1202 and passed migrations, schema/collation, API proxy and + browser shell checks. +- The packaged operator staged official edition `2025.04` for bbox + `5.110,51.180,5.117,51.185`: one 921,848-byte official WMS response became a + 496x562, three-band EPSG:31370 GeoTIFF at an approximately one-metre grid. + All 20 deterministic flight-day samples reported `2025-04-05`. +- The generated preview was visually inspected as a complete RGB orthophoto + with expected Mol roads, buildings and sports context. Named review bound + plan SHA-256 `c6cefd19dc60d7469b4885d9d1f5eff4e52a88808bbe3ac21879c5650e558662` + and review SHA-256 + `9f0a3a65eb184fea7f91daa5018c0980ed03b792bd465a98c65f2add2413d90b`. +- Checksum-confirmed apply created exactly one immutable raster Dataset + `3cf14603-03da-42f5-b152-4bbecad7d006`; project total moved from 636 to 637. + Dataset, version and staged GeoTIFF share SHA-256 + `f87701de6680f8ad25407996ddfd08056bf0533c05572ae489e9d478a72e7e46`. + The canonical API returns the complete request, catalog, flight-date, + reviewer and artifact provenance. +- An idempotent retry retained 637 Datasets and the same Dataset ID. A live + audit exposed that the applied-evidence timestamp could still be rewritten; + write-once validation was added, tested, redeployed and the evidence file + remained byte-identical on the next retry at SHA-256 + `f4ae7b2315aed9370c8ca04dcb57eabf0c0e49c40cf63a862bb4e69bacd4b16d`. +- Browser validation at 1920x911 showed a correctly rendered MapLibre canvas, + coherent regional layer workflow, no horizontal overflow and no console + warnings or errors. A final live API audit found the generic source status + still selected a retained rolling label; source-freshness now prefers the + official edition without changing the 180-day review policy. Next: -- Run complete readiness, deploy the packaged operator to Tower, inspect the - live bounded Mol review preview and only then apply the first official - `2025.04` immutable baseline with explicit review evidence. +- Surface the existing release evidence read-only in the operator Status + workspace so official/local edition, review identity, Dataset ID and hash + integrity can be inspected without shell access. Keep all refresh actions + operator-only and outside the browser.