From 0c2ebc1ffca89cdd8c23d8c042f197199b281086 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 17 Jul 2026 02:23:46 +0200 Subject: [PATCH] Keep orthophoto release evidence immutable --- CHANGELOG.md | 3 ++ ...sprint231_orthophoto_release_management.py | 44 +++++++++++++++++++ docs/CODEX_EXECUTION_LOG.md | 2 + docs/STORAGE_ARCHITECTURE.md | 4 +- scripts/manage_orthophoto_release.py | 29 +++++++++++- 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f271fab..485f744a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ - Made official `YYYY.NN` orthophoto Datasets take precedence over rolling acquisition markers in source-catalog comparison. No API, migration, scheduler, browser fetch or automatic refresh was added. +- 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. ## Sprint 230 Governed orthophoto release preflight (2026-07-17) diff --git a/backend/tests/test_sprint231_orthophoto_release_management.py b/backend/tests/test_sprint231_orthophoto_release_management.py index c4bbbc04..989cfa41 100644 --- a/backend/tests/test_sprint231_orthophoto_release_management.py +++ b/backend/tests/test_sprint231_orthophoto_release_management.py @@ -277,6 +277,9 @@ def test_stage_fetches_one_allowlisted_map_and_writes_reviewable_rgb_evidence(tm assert manifest["normalized_geotiff"]["crs"] == "EPSG:31370" assert manifest["normalized_geotiff"]["band_count"] == 3 assert Path(manifest["review_preview"]["path"]).read_bytes().startswith(b"\x89PNG") + with pytest.raises(RuntimeError, match="will not be overwritten"): + MANAGER.stage_artifacts(args, release_report, request, opener=opener) + assert len(calls) == 1 def test_getmap_redirect_and_response_limits_fail_closed(tmp_path: Path) -> None: @@ -397,6 +400,47 @@ def test_official_flight_dates_are_persisted_without_inventing_a_catalog_date() MANAGER.parse_flight_dates(["spring 2025"]) +def test_applied_evidence_is_immutable_on_idempotent_retry(tmp_path: Path) -> None: + args, plan_path, plan, staged = staged_plan(tmp_path) + manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"])) + args.approve = True + args.reviewer = "Jens" + review = MANAGER.build_review_evidence(args, plan_path, plan, manifest) + review_path = MANAGER.default_review_path(args, "2025.04") + dataset = { + "id": "dataset-1", + "checksum_sha256": manifest["normalized_geotiff"]["sha256"], + } + final_report = report(status="current", local="2025.04") + final_report["release"]["comparison_status"] = "same" + + path, first = MANAGER.applied_evidence( + args, + plan_path, + plan, + review_path, + review, + dataset, + final_report, + reused=False, + ) + original_bytes = path.read_bytes() + _, second = MANAGER.applied_evidence( + args, + plan_path, + plan, + review_path, + review, + dataset, + final_report, + reused=True, + ) + + assert second == first + assert path.read_bytes() == original_bytes + assert second["dataset_status"] == "imported" + + def test_release_manager_is_packaged_and_compiled_by_readiness() -> None: dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8") diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index a1c0fee5..9a1be236 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -9787,6 +9787,8 @@ Implemented: - Made the latest official `YYYY.NN` Dataset authoritative for source-catalog comparison even when a newer-imported rolling marker also exists. No API, migration, release table, Job type or frontend behavior changed. +- Made stage, review and applied evidence write-once. Exact apply retries reuse + 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. diff --git a/docs/STORAGE_ARCHITECTURE.md b/docs/STORAGE_ARCHITECTURE.md index 18014b89..5b645cc6 100644 --- a/docs/STORAGE_ARCHITECTURE.md +++ b/docs/STORAGE_ARCHITECTURE.md @@ -168,7 +168,9 @@ flight-day preflight identities; review binds a named approval; applied evidence binds both to the immutable Dataset id and checksum. Paths outside this root, changed files and changed provider/local state fail closed. Only the normalized GeoTIFF enters ordinary Dataset storage through DatasetService. -Existing `most_recent_at_*` raster metadata is never rewritten. +Existing release evidence and `most_recent_at_*` raster metadata are never +rewritten. An exact apply retry validates and returns the original applied +evidence rather than replacing it. DHMV II DTM/DSM outputs are also normal raster Dataset files. The provider WCS returns multipart coverage data; GeoIntel retains response and extracted diff --git a/scripts/manage_orthophoto_release.py b/scripts/manage_orthophoto_release.py index ff23ac72..d3a1d164 100644 --- a/scripts/manage_orthophoto_release.py +++ b/scripts/manage_orthophoto_release.py @@ -446,6 +446,10 @@ def stage_artifacts( geotiff_path = directory / f"orthophoto_{edition}_{selection_key(list(args.bbox))}.tif" preview_path = directory / "review-preview.png" manifest_path = directory / "staged-manifest.json" + if directory.exists() and any(directory.iterdir()): + raise RuntimeError( + f"Governed orthophoto evidence already exists and will not be overwritten: {directory}" + ) max_bytes = args.max_response_mb * 1024 * 1024 content, content_type, final_url = fetch_map( request_identity, @@ -888,6 +892,22 @@ def applied_evidence( *, reused: bool, ) -> tuple[Path, dict[str, Any]]: + path = plan_path.with_name("applied-evidence.json") + if path.is_file(): + existing = json.loads(path.read_text(encoding="utf-8")) + if existing.get("applied_evidence_sha256") != canonical_sha256(existing, "applied_evidence_sha256"): + raise RuntimeError("Existing orthophoto applied evidence checksum is invalid") + if ( + existing.get("project_id") != args.project_id + or existing.get("scope") != args.scope + or existing.get("edition") != plan["edition"] + or existing.get("staged_plan_sha256") != plan["plan_sha256"] + or existing.get("review_sha256") != review["review_sha256"] + or existing.get("dataset_id") != dataset["id"] + or existing.get("dataset_checksum_sha256") != dataset["checksum_sha256"] + ): + raise RuntimeError("Existing orthophoto applied evidence does not match this approved Dataset") + return path, existing payload: dict[str, Any] = { "schema_version": 1, "status": "applied", @@ -908,7 +928,6 @@ def applied_evidence( "existing_snapshots_retained": True, } payload["applied_evidence_sha256"] = canonical_sha256(payload, "applied_evidence_sha256") - path = plan_path.with_name("applied-evidence.json") write_json(path, payload) return path, payload @@ -939,10 +958,12 @@ def main() -> int: edition = require_edition(args, report) if args.action == "stage": authorization = authorize_stage(args, report) + plan_path = governed_path(args, args.plan_path or default_plan_path(args, edition)) + if plan_path.exists(): + raise RuntimeError(f"Governed orthophoto plan already exists and will not be overwritten: {plan_path}") request_identity = map_request(report) staged = stage_artifacts(args, report, request_identity) plan = build_staged_plan(args, report, authorization, staged) - plan_path = governed_path(args, args.plan_path or default_plan_path(args, edition)) write_json(plan_path, plan) print( json.dumps( @@ -958,6 +979,10 @@ def main() -> int: if args.action == "review": review = build_review_evidence(args, plan_path, plan, manifest) review_path = governed_path(args, args.review_path or default_review_path(args, edition)) + if review_path.exists(): + raise RuntimeError( + f"Governed orthophoto review already exists and will not be overwritten: {review_path}" + ) write_json(review_path, review) print(json.dumps({"status": "approved", "review_path": str(review_path), **review}, ensure_ascii=False, indent=2)) return 0