feat(provenance): govern source snapshots and data inputs

This commit is contained in:
Jens
2026-08-01 23:46:17 +02:00
parent cebeb5f3b4
commit 5b3c17b494
96 changed files with 20156 additions and 351 deletions
+19
View File
@@ -0,0 +1,19 @@
"""Canonical backend-test import boundary.
Pytest is intentionally runnable from ``backend/`` because that is the CI
entrypoint. Some contract tests exercise repository-level deterministic
scripts; put the canonical repository root ahead of the legacy
``backend/scripts`` helper directory so those imports resolve to the code that
is actually shipped by the root Docker build.
"""
from __future__ import annotations
import sys
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
repository_root_text = str(REPOSITORY_ROOT)
if repository_root_text not in sys.path:
sys.path.insert(0, repository_root_text)
@@ -0,0 +1,34 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "run_accuracy_phase2_foundation_audit.py"
SPEC = importlib.util.spec_from_file_location("accuracy_phase2_foundation_audit", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
def test_phase2_foundation_audit_enumerates_exact_source_and_contract_policies() -> None:
payload = MODULE.collect()
assert payload["phase"] == "P2"
assert payload["migration_revision"] == "202608010001"
assert payload["source_registry"]["definition_count"] >= 40
assert payload["source_registry"]["required_building_policy"] == {
"grb_primary_building_validation": "primary",
"buildings_register_classification": "authoritative",
"sentinel_2_classification": "contextual",
"dhmv_classification": "authoritative",
"osm_ground_truth_allowed": False,
}
assert {(item["key"], item["version"]) for item in payload["data_contracts"]} == {
("geointel.vector.geojson", "1.0.0"),
("geointel.raster.geotiff", "1.0.0"),
("geointel.label.yolo", "1.0.0"),
("geointel.label.yolo", "1.1.0"),
("geointel.model.pytorch", "1.0.0"),
}
@@ -0,0 +1,772 @@
from __future__ import annotations
from datetime import timedelta
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from uuid import uuid4
import pytest
from sqlalchemy import CheckConstraint
from app.core.errors import AppError
from app.models import (
Dataset,
DatasetLineageEdge,
DatasetQuarantine,
DatasetVersion,
SourceRegistry,
SourceSnapshot,
)
from app.services.coverage_registry_service import SOURCE_DEFINITIONS
from app.services.dataset_consumption_gate_service import DatasetConsumptionGate
from app.services.dataset_service import DatasetService
from app.services.source_registry_service import (
SERVER_OWNED_SOURCE_DEFINITIONS,
SourceRegistryService,
)
class _Query:
def __init__(self, session: "InMemorySession", model: type) -> None:
self.session = session
self.model = model
self.predicates = []
def filter(self, *predicates):
self.predicates.extend(predicates)
return self
def one_or_none(self):
matches = self._matches()
if len(matches) > 1:
raise AssertionError(
f"Expected one {self.model.__name__}, found {len(matches)}"
)
return matches[0] if matches else None
def all(self):
return self._matches()
def _matches(self):
matches = list(self.session.objects.get(self.model, []))
for predicate in self.predicates:
field_name = predicate.left.key
expected = predicate.right.value
operator_name = getattr(predicate.operator, "__name__", "")
if operator_name == "in_op":
matches = [
item for item in matches if getattr(item, field_name) in expected
]
else:
matches = [
item for item in matches if getattr(item, field_name) == expected
]
return matches
class InMemorySession:
def __init__(self, *objects: object) -> None:
self.objects: dict[type, list[object]] = {}
self.added: list[object] = []
self.flushes = 0
for item in objects:
self._store(item)
def query(self, model: type) -> _Query:
return _Query(self, model)
def add(self, item: object) -> None:
if getattr(item, "id", None) is None:
setattr(item, "id", uuid4())
self._store(item)
self.added.append(item)
def flush(self) -> None:
self.flushes += 1
def _store(self, item: object) -> None:
self.objects.setdefault(type(item), []).append(item)
def _registry(source_key: str) -> SourceRegistry:
definition = SERVER_OWNED_SOURCE_DEFINITIONS[source_key]
return SourceRegistry(id=uuid4(), **definition.as_model_values())
def _dataset() -> Dataset:
return Dataset(
id=uuid4(),
project_id=uuid4(),
name="candidate.tif",
dataset_type="raster",
source="governed",
status="ready",
validation_status="not_validated",
provenance_status="incomplete",
lineage_status="incomplete",
quarantine_status="not_quarantined",
)
def test_server_owned_definitions_encode_building_authority_and_non_ground_truth_sources() -> (
None
):
grb = SERVER_OWNED_SOURCE_DEFINITIONS["grb"]
buildings_register = SERVER_OWNED_SOURCE_DEFINITIONS[
"digitaal_vlaanderen_buildings_addresses_register"
]
sentinel = SERVER_OWNED_SOURCE_DEFINITIONS["sentinel_2"]
dhmv = SERVER_OWNED_SOURCE_DEFINITIONS["digitaal_vlaanderen_dhmv"]
osm = SERVER_OWNED_SOURCE_DEFINITIONS["osm"]
assert grb.classification == "authoritative"
assert grb.usage_policy["ground_truth_allowed"] is True
assert grb.usage_policy["validation_authority"]["building_validation"] == "primary"
assert buildings_register.classification == "authoritative"
assert buildings_register.usage_policy["ground_truth_allowed"] is False
assert (
buildings_register.usage_policy["validation_authority"]["building_validation"]
== "corroborative"
)
assert (
buildings_register.usage_policy["validation_authority"][
"building_register_validation"
]
== "primary"
)
assert sentinel.classification == "contextual"
assert dhmv.classification == "authoritative"
assert dhmv.usage_policy["ground_truth_allowed"] is False
assert (
dhmv.usage_policy["validation_authority"]["building_validation"]
== "corroborative"
)
assert (
dhmv.usage_policy["validation_authority"]["elevation_validation"] == "primary"
)
assert osm.classification == "contextual"
assert osm.usage_policy["ground_truth_allowed"] is False
assert osm.usage_policy["automatic_ground_truth"] is False
assert osm.usage_policy["training_allowed"] is False
assert {
"ngi_adminvector",
"rbins_marine_reporting_units",
"rbins_msp_2026",
"grb",
"digitaal_vlaanderen",
"vrbg",
"digitaal_vlaanderen_buildings_addresses_register",
"digitaal_vlaanderen_orthophoto",
"spw_orthophoto",
"urbis_orthophoto",
"digitaal_vlaanderen_dhmv",
"spw_terrain",
"spw_walous_land_cover",
"spw_geoportail",
"spw_picc",
"urbis",
"vmm_flood_hazard",
"vmm_vha_bathymetry_profiles",
"dov_soil_map",
"statbel",
"waterinfo",
"agentschap_landbouw_zeevisserij_agricultural_parcels",
"sentinel_2",
"osm",
"manual",
"fixture",
"map_selection",
"derived",
"training_label",
"model",
"experimental",
"mdk_bcp_bathymetry",
}.issubset(SERVER_OWNED_SOURCE_DEFINITIONS)
for umbrella_key in ("digitaal_vlaanderen", "spw_geoportail"):
definition = SERVER_OWNED_SOURCE_DEFINITIONS[umbrella_key]
assert definition.classification == "authoritative"
assert definition.usage_policy["ground_truth_allowed"] is False
assert definition.usage_policy["automatic_ground_truth"] is False
assert (
SERVER_OWNED_SOURCE_DEFINITIONS["mdk_bcp_bathymetry"].ingest_status
== "not_configured"
)
def test_coverage_and_direct_adapter_source_keys_are_registry_backed() -> None:
coverage_source_keys = {
definition.contract.source_name for definition in SOURCE_DEFINITIONS
}
coverage_materialization_keys = {
source_key
for definition in SOURCE_DEFINITIONS
for source_key in definition.materialized_source_names
}
direct_adapter_source_keys = {
"digitaal_vlaanderen",
"spw_geoportail",
"mdk_bcp_bathymetry",
}
assert (
coverage_source_keys
| coverage_materialization_keys
| direct_adapter_source_keys
<= set(SERVER_OWNED_SOURCE_DEFINITIONS)
)
def test_new_adapter_source_seed_rows_match_server_owned_registry_semantics() -> None:
migration_path = (
Path(__file__).resolve().parents[1]
/ "alembic"
/ "versions"
/ "202608010001_source_registry_provenance.py"
)
spec = spec_from_file_location("phase2_source_registry_migration", migration_path)
assert spec is not None and spec.loader is not None
migration = module_from_spec(spec)
spec.loader.exec_module(migration)
seed_rows = {row["source_key"]: row for row in migration._seed_rows()}
for source_key in ("digitaal_vlaanderen", "spw_geoportail", "mdk_bcp_bathymetry"):
expected = SERVER_OWNED_SOURCE_DEFINITIONS[source_key].as_model_values()
observed = seed_rows[source_key]
for field in (
"source_key",
"display_name",
"classification",
"authority_name",
"authority_scope_json",
"provider_adapter_key",
"source_url",
"default_crs",
"default_units",
"geographic_coverage_json",
"usage_policy_json",
"freshness_status",
"ingest_status",
"known_limitations_json",
):
assert observed[field] == expected[field]
def test_ensure_source_is_idempotent_and_rejects_caller_owned_unknown_sources() -> None:
grb = _registry("grb")
session = InMemorySession(grb)
assert SourceRegistryService.ensure_server_owned_source(session, "GRB") is grb
assert session.added == []
with pytest.raises(AppError) as exc_info:
SourceRegistryService.ensure_server_owned_source(session, "caller_claimed_grb")
assert exc_info.value.code == "SOURCE_REGISTRY_ENTRY_NOT_FOUND"
def test_snapshot_is_checksum_bound_and_idempotent() -> None:
grb = _registry("grb")
session = InMemorySession(grb)
checksum = "a" * 64
snapshot = SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key="2026-08-01-gbg",
checksum_sha256=checksum,
source_version="2026-08-01",
crs="EPSG:31370",
units="metres",
)
assert snapshot.source_registry_id == grb.id
assert snapshot.checksum_sha256 == checksum
assert snapshot.ingest_status == "ingested"
assert (
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key="2026-08-01-gbg",
checksum_sha256=checksum,
)
is snapshot
)
with pytest.raises(AppError) as exc_info:
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key="2026-08-01-gbg",
checksum_sha256="b" * 64,
)
assert exc_info.value.code == "SOURCE_SNAPSHOT_IMMUTABILITY_CONFLICT"
with pytest.raises(AppError) as version_conflict:
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key="2026-08-01-gbg",
checksum_sha256=checksum,
source_version="2026-08-02",
)
assert version_conflict.value.code == "SOURCE_SNAPSHOT_IMMUTABILITY_CONFLICT"
with pytest.raises(AppError) as invalid_checksum:
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key="bad-checksum",
checksum_sha256="not-a-checksum",
)
assert invalid_checksum.value.code == "SOURCE_SNAPSHOT_CHECKSUM_INVALID"
def test_governed_import_reuses_an_identical_snapshot_without_rewriting_fetched_at() -> None:
"""A second project may bind the same immutable source snapshot safely."""
grb = _registry("grb")
session = InMemorySession(grb)
checksum = "a" * 64
observed_at = None
metadata = {
"dataset_type": "vector",
"bounds_json": {"min_x": 4.0, "min_y": 50.0, "max_x": 4.1, "max_y": 50.1},
}
source_metadata = {"source_url": "https://example.test/grb", "units": "metres"}
# Dataset ingest keys are project-scoped, while a source snapshot is
# globally keyed by immutable source evidence. This represents the same
# source file arriving through two independently resumable imports.
project_one, project_two = uuid4(), uuid4()
assert (
DatasetService._ingest_key(
project_id=project_one,
source_key="grb",
checksum_sha256=checksum,
dataset_type="vector",
dataset_role="source",
area_id=None,
reference_layer_name=None,
source_version="2026-08-01",
)
!= DatasetService._ingest_key(
project_id=project_two,
source_key="grb",
checksum_sha256=checksum,
dataset_type="vector",
dataset_role="source",
area_id=None,
reference_layer_name=None,
source_version="2026-08-01",
)
)
first_source, first_snapshot = DatasetService._record_snapshot(
session,
source_key="grb",
checksum_sha256=checksum,
source_version="2026-08-01",
observed_at=observed_at,
valid_from=None,
valid_to=None,
source_crs="EPSG:31370",
source_metadata=source_metadata,
metadata=metadata,
)
original_fetched_at = first_snapshot.fetched_at
replay_source, replay_snapshot = DatasetService._record_snapshot(
session,
source_key="grb",
checksum_sha256=checksum,
source_version="2026-08-01",
observed_at=observed_at,
valid_from=None,
valid_to=None,
source_crs="EPSG:31370",
source_metadata=source_metadata,
metadata=metadata,
)
assert replay_source is first_source
assert replay_snapshot is first_snapshot
assert replay_snapshot.fetched_at == original_fetched_at
assert session.objects[SourceSnapshot] == [first_snapshot]
# Outside the governed replay path, a contradictory acquisition timestamp
# remains immutable evidence and is still rejected.
with pytest.raises(AppError) as fetched_at_conflict:
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key=first_snapshot.snapshot_key,
checksum_sha256=checksum,
fetched_at=original_fetched_at + timedelta(seconds=1),
)
assert fetched_at_conflict.value.code == "SOURCE_SNAPSHOT_IMMUTABILITY_CONFLICT"
# Replay mode is narrow: a changed immutable evidence field still fails.
with pytest.raises(AppError) as conflict:
SourceRegistryService.record_snapshot(
session,
source_key="grb",
snapshot_key=first_snapshot.snapshot_key,
checksum_sha256=checksum,
crs="EPSG:4326",
reuse_existing_snapshot=True,
)
assert conflict.value.code == "SOURCE_SNAPSHOT_IMMUTABILITY_CONFLICT"
def test_snapshot_schema_requires_a_canonical_sha256() -> None:
constraints = {
constraint.name: str(constraint.sqltext)
for constraint in SourceSnapshot.__table__.constraints
if isinstance(constraint, CheckConstraint)
}
assert SourceSnapshot.__table__.c.checksum_sha256.nullable is False
assert "ck_source_snapshots_checksum_sha256" in constraints
assert (
"lower(checksum_sha256)" in constraints["ck_source_snapshots_checksum_sha256"]
)
def test_complete_provenance_binding_is_required_before_authoritative_validation() -> (
None
):
grb = _registry("grb")
snapshot = SourceSnapshot(
id=uuid4(),
source_registry_id=grb.id,
snapshot_key="governed-grb",
checksum_sha256="c" * 64,
ingest_status="ingested",
)
dataset = _dataset()
SourceRegistryService.bind_dataset_provenance(
dataset,
source=grb,
snapshot=snapshot,
data_contract_key="vector.grb.buildings",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="complete",
)
assert SourceRegistryService.is_dataset_eligible_for_authoritative_validation(
dataset,
source=grb,
snapshot=snapshot,
task="building_validation",
)
osm = _registry("osm")
dataset.source_registry_id = osm.id
snapshot.source_registry_id = osm.id
assert not SourceRegistryService.is_dataset_eligible_for_authoritative_validation(
dataset,
source=osm,
snapshot=snapshot,
task="building_validation",
)
def test_lineage_and_quarantine_are_fail_closed_and_observable() -> None:
session = InMemorySession()
parent_id = uuid4()
child_id = uuid4()
edge = SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=parent_id,
child_dataset_id=child_id,
relation_type="derived_from",
transformation_name="vector_clip",
input_checksum_sha256="d" * 64,
output_checksum_sha256="e" * 64,
)
assert isinstance(edge, DatasetLineageEdge)
assert (
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=parent_id,
child_dataset_id=child_id,
relation_type="derived_from",
transformation_name="vector_clip",
input_checksum_sha256="d" * 64,
output_checksum_sha256="e" * 64,
)
is edge
)
with pytest.raises(AppError) as self_reference:
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=parent_id,
child_dataset_id=parent_id,
relation_type="derived_from",
transformation_name="vector_clip",
)
assert self_reference.value.code == "DATASET_LINEAGE_SELF_REFERENCE"
grandchild_id = uuid4()
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=child_id,
child_dataset_id=grandchild_id,
relation_type="derived_from",
transformation_name="vector_buffer",
)
with pytest.raises(AppError) as cycle:
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=grandchild_id,
child_dataset_id=parent_id,
relation_type="derived_from",
transformation_name="vector_union",
)
assert cycle.value.code == "DATASET_LINEAGE_CYCLE_DETECTED"
dataset = _dataset()
record = SourceRegistryService.quarantine_dataset(
session,
dataset=dataset,
stage="vector_ingest",
reason_code="CRS_UNVERIFIED",
details={"observed_crs": None},
)
assert isinstance(record, DatasetQuarantine)
assert dataset.status == "quarantined"
assert dataset.quarantine_status == "quarantined"
assert dataset.validation_status == "failed"
version_parent = _dataset()
version = DatasetVersion(id=uuid4(), dataset_id=version_parent.id, version=1)
version_session = InMemorySession(version_parent, version)
version_record = SourceRegistryService.quarantine_dataset(
version_session,
dataset_version=version,
stage="dataset_version_validation",
reason_code="CHECKSUM_MISMATCH",
)
assert version_record.dataset_id == version_parent.id
assert version_record.dataset_version_id == version.id
assert version_parent.status == "quarantined"
assert version_parent.quarantine_status == "quarantined"
assert version_parent.validation_status == "failed"
assert version_parent.provenance_status == "incomplete"
assert version_parent.lineage_status == "incomplete"
assert version.validation_status == "failed"
assert version.provenance_status == "incomplete"
snapshot = SourceSnapshot(
id=uuid4(),
source_registry_id=uuid4(),
snapshot_key="quarantined-source",
checksum_sha256="f" * 64,
ingest_status="ingested",
)
SourceRegistryService.quarantine_dataset(
session,
source_snapshot=snapshot,
stage="source_snapshot_validation",
reason_code="CHECKSUM_MISMATCH",
)
assert snapshot.ingest_status == "quarantined"
def test_quarantine_propagates_transitively_to_descendant_dataset_and_version_consumption_gates() -> (
None
):
"""A -> B -> C must fail closed when the governing A artifact is rejected."""
source = _registry("grb")
snapshot = SourceSnapshot(
id=uuid4(),
source_registry_id=source.id,
snapshot_key="transitive-quarantine-source",
checksum_sha256="a" * 64,
freshness_status="current",
ingest_status="ingested",
)
def governed_dataset(name: str) -> Dataset:
dataset = _dataset()
dataset.name = name
dataset.source = "grb"
dataset.source_name = "grb"
dataset.dataset_role = "source"
dataset.checksum_sha256 = snapshot.checksum_sha256
dataset.source_registry_id = source.id
dataset.source_snapshot_id = snapshot.id
dataset.data_contract_key = "geointel.raster.geotiff"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "complete"
dataset.quarantine_status = "not_quarantined"
dataset.status = "ready"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
parent = governed_dataset("parent.tif")
child = governed_dataset("child.tif")
grandchild = governed_dataset("grandchild.tif")
parent_version = DatasetVersion(
id=uuid4(),
dataset_id=parent.id,
version=1,
validation_status="passed",
provenance_status="complete",
lineage_status="complete",
)
child_version = DatasetVersion(
id=uuid4(),
dataset_id=child.id,
version=1,
validation_status="passed",
provenance_status="complete",
lineage_status="complete",
)
grandchild_version = DatasetVersion(
id=uuid4(),
dataset_id=grandchild.id,
version=1,
validation_status="passed",
provenance_status="complete",
lineage_status="complete",
)
session = InMemorySession(
parent,
child,
grandchild,
parent_version,
child_version,
grandchild_version,
)
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=parent.id,
child_dataset_id=child.id,
relation_type="derived_from",
transformation_name="clip",
)
SourceRegistryService.record_lineage_edge(
session,
parent_dataset_id=child.id,
child_dataset_id=grandchild.id,
relation_type="derived_from",
transformation_name="buffer",
)
assert (
DatasetConsumptionGate.evaluate(child, purpose="production_inference").eligible
is True
)
assert (
DatasetConsumptionGate.evaluate(
grandchild, purpose="production_inference"
).eligible
is True
)
SourceRegistryService.quarantine_dataset(
session,
dataset=parent,
stage="contract_validation",
reason_code="CHECKSUM_MISMATCH",
)
for dataset in (parent, child, grandchild):
decision = DatasetConsumptionGate.evaluate(
dataset, purpose="production_inference"
)
assert dataset.status == "quarantined"
assert dataset.quarantine_status == "quarantined"
assert dataset.validation_status == "failed"
assert dataset.provenance_status == "incomplete"
assert dataset.lineage_status == "incomplete"
assert decision.eligible is False
assert "dataset_quarantined" in decision.reasons
for dataset_version in (parent_version, child_version, grandchild_version):
assert dataset_version.validation_status == "failed"
assert dataset_version.provenance_status == "incomplete"
assert dataset_version.lineage_status == "incomplete"
def test_ingest_keys_are_scoped_and_migration_keeps_unknown_legacy_unbound() -> None:
project_id = uuid4()
dataset = _dataset()
dataset.project_id = project_id
dataset.ingest_key = "grb:2026-08-01:gbg:area-sha"
version = DatasetVersion(
id=uuid4(),
dataset_id=dataset.id,
ingest_key=dataset.ingest_key,
validation_status="not_validated",
provenance_status="incomplete",
lineage_status="incomplete",
)
session = InMemorySession(dataset, version)
assert (
SourceRegistryService.find_dataset_by_ingest_key(
session, project_id, dataset.ingest_key
)
is dataset
)
assert (
SourceRegistryService.find_dataset_version_by_ingest_key(
session, dataset.id, dataset.ingest_key
)
is version
)
with pytest.raises(AppError) as invalid_key:
SourceRegistryService.find_dataset_by_ingest_key(session, project_id, " ")
assert invalid_key.value.code == "INGEST_KEY_INVALID"
migration = (
Path(__file__).resolve().parents[1]
/ "alembic"
/ "versions"
/ "202608010001_source_registry_provenance.py"
).read_text(encoding="utf-8")
assert "uuid_generate_v5" not in migration
assert "__unregistered_legacy_source__" in migration
assert "uq_datasets_project_ingest_key" in migration
assert "uq_dataset_versions_dataset_ingest_key" in migration
def test_migration_contains_database_guards_for_snapshot_pairing_contract_lineage_and_quarantine() -> (
None
):
migration = (
Path(__file__).resolve().parents[1]
/ "alembic"
/ "versions"
/ "202608010001_source_registry_provenance.py"
).read_text(encoding="utf-8")
assert "trg_datasets_snapshot_registry_guard" in migration
assert "trg_dataset_versions_snapshot_registry_guard" in migration
assert "trg_source_registry_write_guard" in migration
assert "trg_source_snapshots_evidence_immutable" in migration
assert "geointel_phase2_contract_report_guard" in migration
assert "trg_datasets_contract_report_guard" in migration
assert "trg_dataset_versions_contract_report_guard" in migration
assert "matching complete validation report" in migration
assert "geointel_phase2_lineage_cycle_guard" in migration
assert "trg_dataset_lineage_edges_cycle_guard" in migration
assert "geointel_phase2_lineage_edge_immutable_guard" in migration
assert "trg_dataset_lineage_edges_immutable" in migration
assert "WITH RECURSIVE descendants" in migration
assert "geointel_phase2_quarantine_lineage_descendants" in migration
assert "geointel_phase2_quarantine_state_guard" in migration
assert "trg_dataset_quarantines_state_guard" in migration
assert "accepted dataset artifact and provenance evidence is immutable" in migration
+4 -1
View File
@@ -154,6 +154,7 @@ def test_guest_login_exposes_models_but_rejects_management_and_cross_project_req
other_project = client.get("/api/v1/projects/00000000-0000-0000-0000-000000000999")
detection_models = client.get("/api/v1/detection/models")
segmentation_models = client.get("/api/v1/segmentation/models")
global_source_registry = client.get("/api/v1/source-registry/grb")
cross_project_runs = client.get(
"/api/v1/detection/runs?project_id=00000000-0000-0000-0000-000000000999"
)
@@ -179,6 +180,8 @@ def test_guest_login_exposes_models_but_rejects_management_and_cross_project_req
assert detection_models.status_code == 200
assert detection_models.json()["data"]["models"]
assert segmentation_models.status_code == 200
assert global_source_registry.status_code == 403
assert global_source_registry.json()["error"] == "GUEST_ROUTE_NOT_AVAILABLE"
assert cross_project_runs.status_code == 403
assert cross_project_runs.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
assert cross_project_coverage.status_code == 403
@@ -186,7 +189,7 @@ def test_guest_login_exposes_models_but_rejects_management_and_cross_project_req
def test_guest_session_tokens_fail_closed_without_a_project_scope(monkeypatch) -> None:
client = auth_client(monkeypatch, guest_access=True)
auth_client(monkeypatch, guest_access=True)
settings = get_settings()
try:
+193 -7
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import importlib.util
import hashlib
import json
import subprocess
import sys
@@ -13,6 +14,94 @@ assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
from training_release_manifest import create_training_release_manifest # noqa: E402
def write_fixture_manifest(path: Path) -> None:
policy = "geointel-training-source-eligibility/v1"
def eligible(sample_slug: str) -> dict[str, object]:
return {
"policy_version": policy,
"eligible": True,
"fixture_mode": True,
"raster": {
"eligible": True,
"reasons": [],
"evidence": {
"dataset_id": f"raster:{sample_slug}",
"checksum_sha256": "a" * 64,
"source_registry_id": "fixture-raster",
"source_snapshot_id": "fixture-raster-snapshot",
},
},
"reference": {
"eligible": True,
"reasons": [],
"evidence": {
"dataset_id": f"reference:{sample_slug}",
"checksum_sha256": "b" * 64,
"source_registry_id": "fixture-reference",
"source_snapshot_id": "fixture-reference-snapshot",
},
},
}
path.write_text(
json.dumps(
{
"training_eligibility": {
"policy_version": policy,
"status": "eligible",
"fixture_mode": True,
},
"samples": [
{
"sample_slug": sample_slug,
"split": split,
"raster_dataset_id": f"raster:{sample_slug}",
"reference_dataset_id": f"reference:{sample_slug}",
"training_eligibility": eligible(sample_slug),
}
for sample_slug, split in (("fixture-train", "train"), ("fixture-val", "val"))
],
}
),
encoding="utf-8",
)
(path.parent / "corpus-freeze.json").write_text(
json.dumps(
{
"schema_version": 2,
"manifest_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
"immutable": True,
"training_eligibility_policy": policy,
"fixture_mode": True,
}
),
encoding="utf-8",
)
def write_fixture_training_release(tmp_path: Path, manifest: Path) -> Path:
dataset_dir = tmp_path / "fixture-dataset"
for split, sample_slug in (("train", "fixture-train"), ("val", "fixture-val")):
image = dataset_dir / "images" / split / f"{sample_slug}.png"
label = dataset_dir / "labels" / split / f"{sample_slug}.txt"
image.parent.mkdir(parents=True, exist_ok=True)
label.parent.mkdir(parents=True, exist_ok=True)
image.write_bytes(split.encode("utf-8"))
label.write_text("0 0.5 0.5 0.2 0.2\n", encoding="utf-8")
yaml_path = dataset_dir / "dataset.yaml"
yaml_path.write_text(
f"path: {dataset_dir}\ntrain: images/train\nval: images/val\nnames:\n 0: building\n",
encoding="utf-8",
)
create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=manifest,
fixture_mode=True,
)
return yaml_path
def test_training_command_is_cuda_deterministic_and_bound_to_frozen_inputs(tmp_path: Path) -> None:
command = MODULE.training_command(
@@ -78,6 +167,7 @@ def test_failed_iteration_builds_train_only_sampling_for_next_checkpoint(tmp_pat
corpus_manifest=tmp_path / "manifest.json",
assessment=tmp_path / "assessment.json",
output_dir=tmp_path / "iteration-001" / "failure-driven-training",
review_audit=tmp_path / "review-audit.json",
)
assert command[1].endswith("build_failure_driven_yolo_sampling.py")
assert command[command.index("--summary") + 1].endswith("train-summary.json")
@@ -103,20 +193,23 @@ def test_dry_run_can_gate_existing_checkpoint_without_training(tmp_path: Path) -
"status": "ok", "low_variance_positive_tile_count": 0,
"label_stats": {"invalid_label_count": 0, "missing_label_file_count": 0},
}))
manifest = tmp_path / "manifest.json"
write_fixture_manifest(manifest)
train_yaml = write_fixture_training_release(tmp_path, manifest)
result = subprocess.run(
[
sys.executable, str(SCRIPT),
"--initial-model", str(tmp_path / "candidate.pt"),
"--train-yaml", str(tmp_path / "dataset.yaml"),
"--train-yaml", str(train_yaml),
"--train-summary", str(tmp_path / "train-summary.json"),
"--dataset-audit", str(audit),
"--train-quality-audit", str(quality),
"--calibration-summary", str(tmp_path / "cal.json"),
"--test-summary", str(tmp_path / "test.json"),
"--background-summary", str(tmp_path / "background.json"),
"--corpus-manifest", str(tmp_path / "manifest.json"),
"--corpus-manifest", str(manifest),
"--output-dir", str(tmp_path / "output"),
"--evaluate-initial-model", "--dry-run",
"--evaluate-initial-model", "--fixture-mode", "--dry-run",
], capture_output=True, text=True, check=False,
)
assert result.returncode == 0
@@ -139,6 +232,9 @@ def test_loop_refuses_failed_dataset_audit(tmp_path: Path) -> None:
"status": "ok", "low_variance_positive_tile_count": 0,
"label_stats": {"invalid_label_count": 0, "missing_label_file_count": 0},
}))
manifest = tmp_path / "manifest.json"
write_fixture_manifest(manifest)
train_yaml = write_fixture_training_release(tmp_path, manifest)
result = subprocess.run(
[
sys.executable,
@@ -146,7 +242,7 @@ def test_loop_refuses_failed_dataset_audit(tmp_path: Path) -> None:
"--initial-model",
str(tmp_path / "base.pt"),
"--train-yaml",
str(tmp_path / "dataset.yaml"),
str(train_yaml),
"--train-summary",
str(tmp_path / "train-summary.json"),
"--dataset-audit",
@@ -160,9 +256,10 @@ def test_loop_refuses_failed_dataset_audit(tmp_path: Path) -> None:
"--background-summary",
str(tmp_path / "background.json"),
"--corpus-manifest",
str(tmp_path / "manifest.json"),
str(manifest),
"--output-dir",
str(tmp_path / "output"),
"--fixture-mode",
],
capture_output=True,
text=True,
@@ -172,7 +269,45 @@ def test_loop_refuses_failed_dataset_audit(tmp_path: Path) -> None:
assert "Dataset audit is not eligible for training" in result.stderr
def test_pending_human_review_does_not_block_objective_training() -> None:
def test_loop_rejects_manifest_without_source_eligibility_before_cuda_training(tmp_path: Path) -> None:
audit = tmp_path / "audit.json"
audit.write_text(json.dumps({
"status": "needs_human_review", "failures": [],
"manifest_immutable": True, "spatial_leakage_status": "ok",
}))
quality = tmp_path / "quality.json"
quality.write_text(json.dumps({
"status": "ok", "low_variance_positive_tile_count": 0,
"label_stats": {"invalid_label_count": 0, "missing_label_file_count": 0},
}))
manifest = tmp_path / "manifest.json"
manifest.write_text(json.dumps({"samples": [{"sample_slug": "unproven"}]}), encoding="utf-8")
result = subprocess.run(
[
sys.executable, str(SCRIPT),
"--initial-model", str(tmp_path / "candidate.pt"),
"--train-yaml", str(tmp_path / "dataset.yaml"),
"--train-summary", str(tmp_path / "train-summary.json"),
"--dataset-audit", str(audit),
"--train-quality-audit", str(quality),
"--calibration-summary", str(tmp_path / "cal.json"),
"--test-summary", str(tmp_path / "test.json"),
"--background-summary", str(tmp_path / "background.json"),
"--corpus-manifest", str(manifest),
"--output-dir", str(tmp_path / "output"),
"--evaluate-initial-model", "--dry-run",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode != 0
assert "manifest_training_eligibility_missing" in result.stderr
def test_pending_human_review_blocks_operational_training() -> None:
audit = {
"status": "needs_human_review",
"failures": [],
@@ -185,7 +320,58 @@ def test_pending_human_review_does_not_block_objective_training() -> None:
"status": "ok", "low_variance_positive_tile_count": 0,
"label_stats": {"invalid_label_count": 0, "missing_label_file_count": 0},
}
assert MODULE.dataset_audit_failures(audit, quality) == []
failures = MODULE.dataset_audit_failures(audit, quality)
assert "unsupported audit status: needs_human_review" in failures
assert "review_complete_not_true" in failures
assert "accepted_human_review_evidence_missing" in failures
def test_fixture_mode_can_relax_review_only_after_fixture_manifest_gate() -> None:
audit = {
"status": "needs_human_review",
"failures": [],
"manifest_immutable": True,
"spatial_leakage_status": "ok",
"review_complete": False,
}
quality = {
"status": "ok", "low_variance_positive_tile_count": 0,
"label_stats": {"invalid_label_count": 0, "missing_label_file_count": 0},
}
assert MODULE.dataset_audit_failures(audit, quality, fixture_mode=True) == []
def test_operational_dataset_audit_must_be_the_one_bound_into_the_release(tmp_path: Path) -> None:
bound = tmp_path / "bound-audit.json"
other = tmp_path / "other-audit.json"
bound.write_text("{}", encoding="utf-8")
other.write_text("{}", encoding="utf-8")
release = {"human_review": {"audit_path": str(bound.resolve())}}
MODULE.assert_dataset_audit_bound_to_release(
release=release,
dataset_audit=bound,
fixture_mode=False,
)
try:
MODULE.assert_dataset_audit_bound_to_release(
release=release,
dataset_audit=other,
fixture_mode=False,
)
except MODULE.TrainingReleaseError as exc:
assert "does not match" in str(exc)
else:
raise AssertionError("unbound dataset audit was accepted")
def test_protected_assessment_feedback_is_terminal_and_cannot_seed_another_yaml() -> None:
assert MODULE.protected_feedback_roles(
{"status": "continue_training_loop", "test": {"aggregate": {}}, "background": None}
) == ["test"]
assert MODULE.protected_feedback_roles(
{"status": "continue_training_loop", "test": None, "background": {"aggregate": {}}}
) == ["background"]
def test_training_audit_still_fails_closed_on_automated_integrity_gates() -> None:
@@ -0,0 +1,449 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from hashlib import sha256
import json
from pathlib import Path
import pytest
from shapely.geometry import box
from app.core.errors import AppError
from app.services.data_contract_validation import (
AttributeRule,
BoundingBox,
ContractKind,
DataAssetValidationInput,
DataContract,
DataContractRegistry,
DataContractValidator,
FreshnessRules,
GeometryRecord,
GeometryRules,
LineageEvidence,
LineageRules,
RasterRules,
RequirementLevel,
Resolution,
ResolutionRules,
TransformationEvidence,
ValidationStatus,
build_default_data_contract_registry,
build_label_validation_input,
build_model_validation_input,
build_raster_ingest_input,
build_vector_ingest_input,
validate_registered_asset,
)
from app.services.data_quarantine_service import AssetUse, DataQuarantineService
FIXTURE_ROOT = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "data-contracts"
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc)
CHECKSUM_A = "a" * 64
def _fixture_json(name: str) -> tuple[bytes, object]:
raw = (FIXTURE_ROOT / name).read_bytes()
return raw, json.loads(raw)
def _checksum(raw: bytes) -> str:
return sha256(raw).hexdigest()
def _lineage_with_transform() -> LineageEvidence:
return LineageEvidence(
transformations=(
TransformationEvidence(
name="epsg31370-to-epsg4326",
version="1.0.0",
checksum_sha256=CHECKSUM_A,
),
),
)
def _vector_input_from_fixture(name: str, *, source_crs: str = "EPSG:31370", storage_crs: str = "EPSG:4326") -> DataAssetValidationInput:
raw, payload = _fixture_json(name)
assert isinstance(payload, dict)
return build_vector_ingest_input(
asset_id=f"fixture:{name}",
source_crs=source_crs,
storage_crs=storage_crs,
feature_collection=payload,
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
source_registry_id="source:digitaal-vlaanderen:grb",
source_snapshot_id="snapshot:grb:2026-07-31",
imported_at=NOW,
metadata={"license": "Open Data Licence", "provider": "Digitaal Vlaanderen"},
observed_at=NOW - timedelta(days=1),
source_version="2026.07.31",
lineage=_lineage_with_transform() if source_crs != storage_crs else LineageEvidence(),
)
def _issue_codes(report) -> set[str]:
return {issue.code for issue in report.issues}
def test_default_vector_contract_accepts_transformed_geojson_with_complete_provenance() -> None:
report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
assert report.validation_status == ValidationStatus.PASSED
assert report.quarantine_status == "not_quarantined"
assert report.provenance_status == "complete"
assert report.lineage_status == "complete"
persisted = report.persistence_fields()
assert persisted["data_contract_key"] == "geointel.vector.geojson"
assert persisted["data_contract_version"] == "1.0.0"
assert persisted["validation_report_json"]["report_sha256"] == report.report_sha256
def test_default_vector_contract_quarantines_lambert_coordinates_mislabelled_as_epsg4326() -> None:
report = validate_registered_asset(
_vector_input_from_fixture(
"vector-lambert-mislabelled-as-4326.geojson",
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
),
now=NOW,
)
assert report.validation_status == ValidationStatus.FAILED
assert report.quarantine_status == "quarantined"
assert "CRS_COORDINATE_DOMAIN_VIOLATION" in _issue_codes(report)
def test_vector_contract_checks_geometry_attributes_bounds_and_topology_fail_closed() -> None:
contract = DataContract(
key="test.vector.buildings",
version="1.0.0",
kind=ContractKind.VECTOR,
accepted_source_crs=frozenset({"EPSG:4326"}),
canonical_storage_crs="EPSG:4326",
spatial_domain=BoundingBox(2.0, 49.0, 7.0, 52.0),
require_bounds=True,
geometry_rules=GeometryRules(
allowed_geometry_types=frozenset({"Polygon"}),
attribute_rules=(AttributeRule("native_id", accepted_types=("integer",)),),
forbid_shared_area=True,
),
)
raw = b"overlapping-vector"
asset = DataAssetValidationInput(
asset_id="vector:bad",
data_contract_key=contract.key,
data_contract_version=contract.version,
kind=ContractKind.VECTOR,
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
bounds=BoundingBox(4.0, 51.0, 4.1, 51.1),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
geometry_records=(
GeometryRecord(box(4.0, 51.0, 4.05, 51.05), {"native_id": "wrong-type"}),
GeometryRecord(box(4.025, 51.025, 4.075, 51.075), {}),
),
source_registry_id="source:test",
source_snapshot_id="snapshot:test",
imported_at=NOW,
)
report = DataContractValidator.validate(contract, asset, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert {"ATTRIBUTE_TYPE_INVALID", "ATTRIBUTE_REQUIRED", "TOPOLOGY_SHARED_AREA"} <= _issue_codes(report)
assert "BOUNDS_GEOMETRY_MISMATCH" in _issue_codes(report)
def test_default_vector_contract_validates_replayable_large_partition_stream_without_materialising_geometry_list() -> None:
"""Regional imports may be large but remain fully schema/domain checked.
The default contract has no source-specific shared-area rule, so the
validator must make its bounds/schema passes over a replayable stream
without accumulating every Shapely geometry in memory. A stricter
source-specific contract can still opt into a bounded topology batch.
"""
class ReplayableRecords:
def __init__(self, count: int) -> None:
self.count = count
self.iterations = 0
def __iter__(self):
self.iterations += 1
for index in range(self.count):
yield GeometryRecord(
box(4.69, 51.09, 4.70, 51.10),
{"partition_feature": index},
)
raw = b"partitioned-vector-stream"
records = ReplayableRecords(12_000)
asset = DataAssetValidationInput(
asset_id="vector:partitioned-stream",
data_contract_key="geointel.vector.geojson",
data_contract_version="1.0.0",
kind=ContractKind.VECTOR,
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
bounds=BoundingBox(4.69, 51.09, 4.70, 51.10),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
metadata={"license": "Open Data"},
geometry_records=records,
source_registry_id="source:grb",
source_snapshot_id="snapshot:grb:partitioned",
imported_at=NOW,
observed_at=NOW,
source_version="2026-08-01",
)
report = validate_registered_asset(asset, now=NOW)
assert report.validation_status == ValidationStatus.PASSED
assert records.iterations >= 2
def test_raster_contract_accepts_explicit_units_and_quarantines_stale_bad_profile() -> None:
raw = b"raster-stage"
valid = build_raster_ingest_input(
asset_id="raster:valid",
source_crs="EPSG:31370",
storage_crs="EPSG:31370",
raster_profile={"width": 512, "height": 512, "band_count": 3, "dtype": ["uint8"]},
bounds=BoundingBox(193_277.5, 205_708.3, 193_777.5, 206_208.3),
resolution=Resolution(0.9765625, 0.9765625, "m"),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
source_registry_id="source:orthophoto",
source_snapshot_id="snapshot:orthophoto:2026.01",
imported_at=NOW,
metadata={"license": "Open Data"},
observed_at=None,
temporal_unknown_reason="latest mosaic has no per-pixel observation date",
source_version=None,
source_version_unknown_reason="provider did not publish an edition",
)
assert validate_registered_asset(valid, now=NOW).validation_status == ValidationStatus.PASSED
strict = DataContract(
key="test.raster.strict",
version="1.0.0",
kind=ContractKind.RASTER,
accepted_source_crs=frozenset({"EPSG:31370"}),
require_bounds=True,
raster_rules=RasterRules(allowed_band_counts=frozenset({3}), allowed_dtypes=frozenset({"uint8"})),
resolution_rules=ResolutionRules(allowed_units=frozenset({"m"}), min_x=0.2, max_x=1.0, min_y=0.2, max_y=1.0),
freshness_rules=FreshnessRules(observed_at=RequirementLevel.REQUIRED, max_age=timedelta(days=30)),
lineage_rules=LineageRules(require_transformation_when_crs_changes=False),
)
invalid = DataAssetValidationInput(
asset_id="raster:bad",
data_contract_key=strict.key,
data_contract_version=strict.version,
kind=ContractKind.RASTER,
source_crs="EPSG:31370",
storage_crs="EPSG:31370",
bounds=BoundingBox(100.0, 100.0, 200.0, 200.0),
checksum_sha256=_checksum(raw),
computed_checksum_sha256=_checksum(raw),
content=raw,
raster_profile={"width": 0, "height": 10, "band_count": 2, "dtype": ["float32"]},
resolution=Resolution(2.0, 0.1, "degree"),
source_registry_id="source:raster",
source_snapshot_id="snapshot:raster",
imported_at=NOW,
observed_at=NOW - timedelta(days=31),
)
report = DataContractValidator.validate(strict, invalid, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert {
"RASTER_PROFILE_VALUE_INVALID",
"RASTER_BAND_COUNT_NOT_ALLOWED",
"RASTER_DTYPE_NOT_ALLOWED",
"RESOLUTION_UNIT_NOT_ALLOWED",
"RESOLUTION_OUT_OF_RANGE",
"FRESHNESS_EXCEEDED",
} <= _issue_codes(report)
def test_default_label_and_model_contracts_validate_good_and_bad_fixtures() -> None:
valid_raw, valid_labels = _fixture_json("labels-valid.json")
invalid_raw, invalid_labels = _fixture_json("labels-invalid.json")
assert isinstance(valid_labels, list)
assert isinstance(invalid_labels, list)
lineage = LineageEvidence(upstream_asset_ids=("image:1",), upstream_checksums_sha256=(CHECKSUM_A,))
valid_label = build_label_validation_input(
asset_id="label:valid",
label_records=valid_labels,
checksum_sha256=_checksum(valid_raw),
computed_checksum_sha256=_checksum(valid_raw),
content=valid_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=lineage,
)
valid_report = validate_registered_asset(valid_label, now=NOW)
assert valid_report.validation_status == ValidationStatus.PASSED
invalid_label = build_label_validation_input(
asset_id="label:invalid",
label_records=invalid_labels,
checksum_sha256=_checksum(invalid_raw),
computed_checksum_sha256=_checksum(invalid_raw),
content=invalid_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": "not-a-sha256",
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=lineage,
)
invalid_report = validate_registered_asset(invalid_label, now=NOW)
assert invalid_report.validation_status == ValidationStatus.FAILED
assert {
"LABEL_CLASS_ID_NOT_ALLOWED",
"LABEL_NORMALIZED_COORDINATE_INVALID",
"METADATA_CHECKSUM_INVALID",
} <= _issue_codes(invalid_report)
pure_background_raw = b""
pure_background_metadata = {
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
"label_mode": "pure_background",
"sample_slug": "forest-background-aoi",
"split": "train",
"raster_dataset_id": "dataset:raster:1",
"reference_dataset_id": "dataset:reference:1",
"review_decision": "accepted",
"reviewer_id": "reviewer@example.test",
"reviewed_at": "2026-08-01T11:00:00+00:00",
"review_artifact_sha256": CHECKSUM_A,
}
pure_background = build_label_validation_input(
asset_id="label:pure-background",
label_records=(),
label_mode="pure_background",
checksum_sha256=_checksum(pure_background_raw),
computed_checksum_sha256=_checksum(pure_background_raw),
content=pure_background_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata=pure_background_metadata,
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=LineageEvidence(
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
),
)
assert validate_registered_asset(pure_background, now=NOW).validation_status == ValidationStatus.PASSED
unmarked_empty = build_label_validation_input(
asset_id="label:unmarked-empty",
label_records=(),
checksum_sha256=_checksum(pure_background_raw),
computed_checksum_sha256=_checksum(pure_background_raw),
content=pure_background_raw,
source_registry_id="source:labels",
source_snapshot_id="snapshot:labels:1",
imported_at=NOW,
metadata={
"image_checksum_sha256": CHECKSUM_A,
"class_ontology_version": "buildings-v1",
"source_corpus_manifest_sha256": CHECKSUM_A,
},
temporal_unknown_reason="labels inherit image observation handling",
source_version_unknown_reason="label release is represented by its snapshot",
lineage=LineageEvidence(
upstream_asset_ids=("dataset:raster:1", "dataset:reference:1"),
upstream_checksums_sha256=(CHECKSUM_A, CHECKSUM_A),
),
)
assert "PURE_BACKGROUND_MODE_REQUIRED" in _issue_codes(validate_registered_asset(unmarked_empty, now=NOW))
model_raw = b"model-asset"
model = build_model_validation_input(
asset_id="model:valid",
model_metadata={"model_format": "pytorch", "framework": "torch", "class_mapping": {"0": "building"}},
checksum_sha256=_checksum(model_raw),
computed_checksum_sha256=_checksum(model_raw),
content=model_raw,
source_registry_id="source:model-registry",
source_snapshot_id="snapshot:model:1",
imported_at=NOW,
source_version="candidate-1",
metadata={"training_manifest_sha256": CHECKSUM_A, "runtime_manifest_sha256": CHECKSUM_A},
lineage=lineage,
)
assert validate_registered_asset(model, now=NOW).validation_status == ValidationStatus.PASSED
def test_unknown_contract_and_quarantine_gate_are_deterministic_and_fail_closed() -> None:
unknown = DataAssetValidationInput(
asset_id="asset:unknown",
data_contract_key="does.not.exist",
data_contract_version="9.9.9",
kind=ContractKind.VECTOR,
)
report = DataContractRegistry().validate(unknown, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert report.quarantine_status == "quarantined"
assert _issue_codes(report) == {"DATA_CONTRACT_UNKNOWN"}
first = DataQuarantineService.decide(report)
second = DataQuarantineService.decide(report)
assert first.idempotency_key == second.idempotency_key
assert first.reason_codes == ("DATA_CONTRACT_UNKNOWN",)
with pytest.raises(AppError, match="cannot enter this pipeline") as exc_info:
DataQuarantineService.require_eligible(first, use=AssetUse.PRODUCTION_INFERENCE)
assert exc_info.value.code == "DATASET_QUARANTINED"
assert exc_info.value.details["use"] == "production_inference"
clean_report = validate_registered_asset(_vector_input_from_fixture("vector-building-valid.geojson"), now=NOW)
release_request = DataQuarantineService.decide(clean_report, previous=first)
assert release_request.quarantine_status == "quarantined"
assert release_request.requires_explicit_release is True
assert release_request.reason_codes == ("QUARANTINE_RELEASE_REQUIRES_EXPLICIT_PERSISTENCE",)
def test_registry_requires_exact_contract_version_and_fingerprints_schema() -> None:
registry = build_default_data_contract_registry()
version_mismatch = _vector_input_from_fixture("vector-building-valid.geojson")
mismatched = DataAssetValidationInput(
**{**version_mismatch.__dict__, "data_contract_version": "2.0.0"},
)
report = registry.validate(mismatched, now=NOW)
assert report.validation_status == ValidationStatus.FAILED
assert "DATA_CONTRACT_UNKNOWN" in _issue_codes(report)
contract = registry.resolve("geointel.vector.geojson", "1.0.0")
assert contract is not None
direct_report = DataContractValidator.validate(contract, mismatched, now=NOW)
assert direct_report.validation_status == ValidationStatus.FAILED
assert "DATA_CONTRACT_IDENTITY_MISMATCH" in _issue_codes(direct_report)
@@ -0,0 +1,316 @@
from __future__ import annotations
from types import SimpleNamespace
from uuid import uuid4
import pytest
from shapely.geometry import box
from app.core.errors import AppError
from app.models import Dataset, SourceRegistry, SourceSnapshot
from app.services.coverage_registry_service import CoverageRegistryService, SOURCE_DEFINITIONS
import app.services.dataset_consumption_gate_service as gate_module
from app.services.dataset_consumption_gate_service import DatasetConsumptionGate
from app.services.export_service import ExportService
def _governed_dataset(
*,
source_key: str = "grb",
classification: str = "authoritative",
snapshot_freshness_status: str = "current",
) -> Dataset:
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
source = SourceRegistry(
id=source_id,
source_key=source_key,
display_name=f"{source_key} test source",
classification=classification,
authority_name="GeoIntel test authority",
authority_scope_json={"scope": "test"},
usage_policy_json={
"ground_truth_allowed": classification == "authoritative",
"validation_authority": {"building_validation": "primary"}
if classification == "authoritative"
else {},
},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key="test-snapshot",
checksum_sha256=checksum,
freshness_status=snapshot_freshness_status,
ingest_status="ingested",
)
dataset = Dataset(
id=uuid4(),
project_id=uuid4(),
name="governed.tif",
dataset_type="raster",
source=source_key,
source_name=source_key,
dataset_role="source",
checksum_sha256=checksum,
source_registry_id=source_id,
source_snapshot_id=snapshot_id,
data_contract_key="geointel.raster.geotiff",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
status="ready",
)
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_governed_dataset_passes_production_inference_and_authoritative_coverage() -> None:
dataset = _governed_dataset()
inference = DatasetConsumptionGate.assert_eligible(dataset, purpose="production_inference")
coverage = DatasetConsumptionGate.assert_eligible(dataset, purpose="authoritative_coverage")
assert inference.eligible is True
assert coverage.eligible is True
@pytest.mark.parametrize(
("field", "value", "error_code"),
(
("provenance_status", "incomplete", "DATASET_PROVENANCE_INCOMPLETE"),
("validation_status", "failed", "DATASET_QUARANTINED"),
("quarantine_status", "quarantined", "DATASET_QUARANTINED"),
),
)
def test_explicit_unsafe_states_can_never_be_relaxed(field: str, value: str, error_code: str) -> None:
dataset = _governed_dataset()
setattr(dataset, field, value)
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(
dataset,
purpose="production_inference",
fixture_mode=True,
)
assert exc_info.value.code == error_code
assert field.replace("_status", "") in " ".join(exc_info.value.details["reasons"])
def test_legacy_fixture_can_support_fixture_qa_but_never_authoritative_coverage() -> None:
fixture = Dataset(
id=uuid4(),
project_id=uuid4(),
name="fixture.tif",
dataset_type="raster",
source="fixture",
)
qa = DatasetConsumptionGate.assert_eligible(fixture, purpose="quality_assessment")
with pytest.raises(AppError) as inference_error:
DatasetConsumptionGate.assert_eligible(
fixture,
purpose="production_inference",
fixture_mode=True,
)
with pytest.raises(AppError) as export_error:
DatasetConsumptionGate.assert_eligible(fixture, purpose="export")
with pytest.raises(AppError) as fixture_export_error:
DatasetConsumptionGate.assert_eligible(fixture, purpose="export", fixture_mode=True)
coverage = DatasetConsumptionGate.evaluate(fixture, purpose="authoritative_coverage")
assert qa.fixture_legacy_exception is True
assert inference_error.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "fixture_qa_only" in inference_error.value.details["reasons"]
assert export_error.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert fixture_export_error.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "fixture_qa_only" in fixture_export_error.value.details["reasons"]
assert coverage.eligible is False
assert "fixture_not_authoritative_coverage" in coverage.reasons
def test_unprovenanced_persistent_dataset_is_blocked(monkeypatch) -> None:
dataset = Dataset(
id=uuid4(),
project_id=uuid4(),
name="manual.tif",
dataset_type="raster",
source="manual_upload",
)
monkeypatch.setattr(gate_module, "sa_inspect", lambda _dataset: SimpleNamespace(transient=False))
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(
dataset,
purpose="production_inference",
fixture_mode=True,
)
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "phase2_provenance_missing" in exc_info.value.details["reasons"]
assert "fixture_source_required" in exc_info.value.details["reasons"]
def test_transient_orm_test_double_can_only_bypass_missing_legacy_fields_for_qa() -> None:
transient = Dataset(
id=uuid4(),
project_id=uuid4(),
name="transient-test.tif",
dataset_type="raster",
source="manual_upload",
)
decision = DatasetConsumptionGate.assert_eligible(transient, purpose="quality_assessment")
coverage = DatasetConsumptionGate.evaluate(transient, purpose="authoritative_coverage")
with pytest.raises(AppError) as production_error:
DatasetConsumptionGate.assert_eligible(transient, purpose="production_inference")
assert decision.fixture_legacy_exception is True
assert production_error.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert coverage.eligible is False
assert "phase2_provenance_missing" in coverage.reasons
@pytest.mark.parametrize("purpose", ("production_inference", "derived_processing", "export"))
def test_passed_manual_or_experimental_dataset_cannot_cross_production_boundary(purpose: str) -> None:
"""A syntactically valid manual upload remains experimental, never production-ready."""
manual = _governed_dataset(source_key="manual", classification="experimental")
manual.source = "manual_upload"
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(manual, purpose=purpose) # type: ignore[arg-type]
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "experimental_source_not_allowed_for_purpose" in exc_info.value.details["reasons"]
def test_reference_validation_requires_authoritative_ground_truth_reference() -> None:
reference = _governed_dataset()
reference.dataset_type = "vector"
reference.dataset_role = "reference"
decision = DatasetConsumptionGate.assert_eligible(
reference,
purpose="reference_validation",
reference_task="building_validation",
)
assert decision.eligible is True
reference.source_registry.classification = "corroborative"
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(
reference,
purpose="reference_validation",
reference_task="building_validation",
)
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "reference_source_not_authoritative" in exc_info.value.details["reasons"]
def test_pending_regional_building_authority_cannot_become_truth_without_approval() -> None:
reference = _governed_dataset(source_key="spw_picc", classification="authoritative")
reference.dataset_type = "vector"
reference.dataset_role = "reference"
reference.source_registry.authority_scope_json = {"zone": "Wallonia"}
reference.source_registry.usage_policy_json = {
"ground_truth_allowed": True,
"validation_authority": {"building_validation": "regional_primary_pending_contract"},
}
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(
reference,
purpose="reference_validation",
reference_task="building_validation",
)
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "reference_task_authority_not_approved" in exc_info.value.details["reasons"]
def test_source_snapshot_must_belong_to_the_dataset_source_registry() -> None:
dataset = _governed_dataset()
dataset.source_snapshot.source_registry_id = uuid4()
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(dataset, purpose="production_inference")
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "source_snapshot_registry_mismatch" in exc_info.value.details["reasons"]
@pytest.mark.parametrize("freshness_status", ("unknown", "review_required", "due", "stale"))
def test_non_consumable_source_snapshot_freshness_is_blocked_at_production_boundaries(
freshness_status: str,
) -> None:
dataset = _governed_dataset(snapshot_freshness_status=freshness_status)
for purpose in ("production_inference", "authoritative_coverage"):
with pytest.raises(AppError) as exc_info:
DatasetConsumptionGate.assert_eligible(dataset, purpose=purpose) # type: ignore[arg-type]
assert exc_info.value.code == "DATASET_PROVENANCE_INCOMPLETE"
assert "source_snapshot_freshness_not_eligible" in exc_info.value.details["reasons"]
def test_coverage_registry_ignores_explicitly_incomplete_materialization() -> None:
definition = next(item for item in SOURCE_DEFINITIONS if item.contract.source_name == "digitaal_vlaanderen")
unsafe_materialization = SimpleNamespace(
id=uuid4(),
status="ready",
source_name="grb",
validation_status="passed",
provenance_status="incomplete",
lineage_status="complete",
quarantine_status="not_quarantined",
)
matches, fully_covered = CoverageRegistryService._matching_datasets(
[unsafe_materialization],
definition,
"buildings",
"flanders",
box(4.0, 50.8, 4.1, 50.9),
)
assert matches == []
assert fully_covered is False
def test_vector_export_is_fail_closed_before_selection(monkeypatch) -> None:
dataset = _governed_dataset()
dataset.dataset_type = "vector"
dataset.status = "quarantined"
queried = False
class _Session:
@staticmethod
def get(model, item_id):
return dataset if model is Dataset and item_id == dataset.id else None
def _unexpected_selection(*_args, **_kwargs):
nonlocal queried
queried = True
raise AssertionError("unsafe dataset must be rejected before querying vector features")
monkeypatch.setattr(
"app.services.export_service.VectorFeatureService.select_features_by_bbox",
_unexpected_selection,
)
with pytest.raises(AppError) as exc_info:
ExportService.export_vector_selection_geojson(
_Session(),
dataset.id,
{"min_x": 4.0, "min_y": 50.0, "max_x": 4.1, "max_y": 50.1, "crs": "EPSG:4326"},
)
assert exc_info.value.code == "DATASET_QUARANTINED"
assert queried is False
@@ -74,6 +74,8 @@ def test_all_in_one_dockerfile_copies_operator_scripts_for_runtime_use() -> None
"audit_operator_yolo_dataset_quality.py",
"render_operator_yolo_label_qa_contact_sheets.py",
"train_operator_yolo_detector.sh",
"training_dataset_eligibility.py",
"training_release_manifest.py",
"verify_real_data_detection_qa_workflow.sh",
"run_detection_quality_matrix.sh",
"run_multi_sample_detection_quality_matrix.sh",
@@ -3,6 +3,8 @@ from __future__ import annotations
import importlib.util
from pathlib import Path
import pytest
SCRIPT = Path(__file__).parents[2] / "scripts" / "build_failure_driven_yolo_sampling.py"
SPEC = importlib.util.spec_from_file_location("failure_sampling", SCRIPT)
@@ -22,7 +24,7 @@ def test_dataset_validation_source_preserves_manifest_path(tmp_path: Path):
assert MODULE.dataset_validation_source(source) == "/data/source/internal-val.txt"
def test_sampling_repeats_only_failed_region_train_tiles() -> None:
def test_sampling_rejects_protected_test_and_background_feedback() -> None:
manifest = {
"samples": [
{"sample_slug": "train-fl", "split": "train", "region": "flanders"},
@@ -54,15 +56,10 @@ def test_sampling_repeats_only_failed_region_train_tiles() -> None:
},
"background": {"pure_empty_false_positives": 2},
}
paths, metadata = MODULE.build_sampling(
summary=summary, manifest=manifest, assessment=assessment, max_region_share=1.0
)
assert paths.count(str(Path("/tmp/fl-pos.png").resolve())) == 3
assert paths.count(str(Path("/tmp/fl-neg.png").resolve())) == 4
assert paths.count(str(Path("/tmp/wa-pos.png").resolve())) == 1
assert not any("protected" in path for path in paths)
assert metadata["protected_samples_in_training"] == []
assert metadata["weak_recall_regions"] == ["flanders"]
with pytest.raises(ValueError, match="protected test/background evidence"):
MODULE.build_sampling(
summary=summary, manifest=manifest, assessment=assessment, max_region_share=1.0
)
def test_sampling_can_use_calibration_before_test_is_opened() -> None:
@@ -171,7 +168,7 @@ def test_sampling_targets_failed_calibration_contexts_without_using_protected_ti
assert metadata["recall_dominant_regions"] == ["flanders"]
def test_recall_dominance_does_not_suppress_negatives_when_background_gate_failed() -> None:
def test_sampling_rejects_background_feedback_after_a_protected_background_opening() -> None:
manifest = {"samples": [
{"sample_slug": "positive", "split": "train", "region": "flanders", "context": "industrial"},
{"sample_slug": "negative", "split": "train", "region": "flanders", "context": "industrial-hard-negative"},
@@ -190,12 +187,10 @@ def test_recall_dominance_does_not_suppress_negatives_when_background_gate_faile
"background": {"pure_empty_false_positives": 1},
}
paths, metadata = MODULE.build_sampling(
summary=summary, manifest=manifest, assessment=assessment, max_region_share=1.0,
)
assert paths.count(str(Path("/tmp/negative.png").resolve())) == 4
assert metadata["recall_dominant_regions"] == []
with pytest.raises(ValueError, match="protected background evidence"):
MODULE.build_sampling(
summary=summary, manifest=manifest, assessment=assessment, max_region_share=1.0,
)
def test_region_cap_drops_only_repeats_and_preserves_every_unique_tile() -> None:
@@ -0,0 +1,765 @@
from __future__ import annotations
from datetime import UTC, datetime
from hashlib import sha256
import json
from pathlib import Path
from uuid import UUID, uuid4
import pytest
from pyproj import Transformer
from app.core.errors import AppError
from app.models import (
Area,
Dataset,
DatasetQuarantine,
DatasetVersion,
Project,
SourceRegistry,
SourceSnapshot,
VectorFeature,
)
from app.services.dataset_service import DatasetService, _PartitionedGeoJsonRecords
from app.services.vector_operations_service import VectorOperationsService
class _Query:
def __init__(self, session: "_Session", model: type) -> None:
self.session = session
self.model = model
self.predicates = []
def filter(self, *predicates):
self.predicates.extend(predicates)
return self
def one_or_none(self):
matches = self._matches()
if len(matches) > 1:
raise AssertionError(
f"expected one {self.model.__name__}, found {len(matches)}"
)
return matches[0] if matches else None
def all(self):
return self._matches()
def _matches(self):
matches = list(self.session.rows.get(self.model, []))
for predicate in self.predicates:
field_name = predicate.left.key
expected = predicate.right.value
operator_name = getattr(predicate.operator, "__name__", "")
if operator_name == "in_op":
matches = [
item for item in matches if getattr(item, field_name) in expected
]
else:
matches = [
item for item in matches if getattr(item, field_name) == expected
]
return matches
class _Session:
"""Small ORM-shaped harness that exercises the real governed path."""
def __init__(self, project: Project) -> None:
self.rows: dict[type, list[object]] = {Project: [project]}
self.commits = 0
self.rollbacks = 0
self.flushes = 0
def get(self, model: type, item_id: UUID):
return next(
(
item
for item in self.rows.get(model, [])
if getattr(item, "id", None) == item_id
),
None,
)
def query(self, model: type) -> _Query:
return _Query(self, model)
def add(self, item: object) -> None:
if getattr(item, "id", None) is None:
setattr(item, "id", uuid4())
self.rows.setdefault(type(item), []).append(item)
def flush(self) -> None:
self.flushes += 1
def commit(self) -> None:
self.commits += 1
def rollback(self) -> None:
self.rollbacks += 1
def refresh(self, _item: object) -> None:
return None
def expunge(self, _item: object) -> None:
return None
def _storage_info(tmp_path: Path, content: bytes) -> dict[str, object]:
path = tmp_path / "grb-buildings.geojson"
path.write_bytes(content)
return {
"storage_path": str(path),
"original_filename": path.name,
"stored_filename": path.name,
"content_type": "application/geo+json",
"size_bytes": len(content),
"checksum_sha256": sha256(content).hexdigest(),
}
def _valid_payload() -> bytes:
return json.dumps(
{
"type": "FeatureCollection",
"crs": {"type": "name", "properties": {"name": "EPSG:4326"}},
"features": [
{
"type": "Feature",
"id": "gbg-1",
"properties": {"id": "gbg-1"},
"geometry": {
"type": "Polygon",
"coordinates": [
[[4.69, 51.09], [4.70, 51.09], [4.70, 51.10], [4.69, 51.09]]
],
},
}
],
}
).encode("utf-8")
def _grb_payload_without_required_id() -> bytes:
return json.dumps(
{
"type": "FeatureCollection",
"crs": {"type": "name", "properties": {"name": "EPSG:4326"}},
"features": [
{
"type": "Feature",
"properties": {"unrelated": "not a GRB identity"},
"geometry": {
"type": "Polygon",
"coordinates": [
[[4.69, 51.09], [4.70, 51.09], [4.70, 51.10], [4.69, 51.09]]
],
},
}
],
}
).encode("utf-8")
def _lambert_grb_payload() -> tuple[bytes, tuple[float, float, float, float]]:
"""Create a valid GRB-shaped source artifact in its declared native CRS."""
longitude, latitude = 4.70, 51.10
max_longitude, max_latitude = 4.7001, 51.1001
to_lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
lambert_ring = [
to_lambert.transform(longitude, latitude),
to_lambert.transform(max_longitude, latitude),
to_lambert.transform(max_longitude, max_latitude),
to_lambert.transform(longitude, latitude),
]
return (
json.dumps(
{
"type": "FeatureCollection",
"crs": {"type": "name", "properties": {"name": "EPSG:31370"}},
"features": [
{
"type": "Feature",
"id": "GBG.lambert.1",
"properties": {"id": "GBG.lambert.1"},
"geometry": {"type": "Polygon", "coordinates": [lambert_ring]},
}
],
}
).encode("utf-8"),
(longitude, latitude, max_longitude, max_latitude),
)
def test_governed_vector_import_persists_snapshot_contract_and_queryable_features(
monkeypatch, tmp_path: Path
) -> None:
project = Project(id=uuid4(), name="Phase 2 governed ingest")
db = _Session(project)
raw = _valid_payload()
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
lambda **_kwargs: _storage_info(tmp_path, raw),
)
result = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="grb-buildings.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={
"license": "Open data",
"source_url": "https://example.invalid/grb",
},
provenance_metadata={"adapter": "test"},
temporal_series_key="grb:2026-08",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01",
temporal_granularity="snapshot",
)
dataset = next(item for item in db.rows[Dataset] if item.id == result.id)
snapshot = db.rows[SourceSnapshot][0]
source = db.rows[SourceRegistry][0]
assert result.status == "ready"
assert dataset.source_name == "grb"
assert dataset.source_registry_id == source.id
assert dataset.source_snapshot_id == snapshot.id
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.lineage_status == "complete"
assert dataset.quarantine_status == "not_quarantined"
assert dataset.crs == "EPSG:4326"
assert snapshot.checksum_sha256 == sha256(raw).hexdigest()
assert len(db.rows[VectorFeature]) == 1
assert db.commits == 1
# A retry with identical governed evidence is idempotent and does not
# create a second source snapshot, dataset or vector feature.
repeated = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="grb-buildings.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={"adapter": "test"},
temporal_series_key="grb:2026-08",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01",
temporal_granularity="snapshot",
)
assert repeated.id == result.id
assert len(db.rows[Dataset]) == 1
assert len(db.rows[SourceSnapshot]) == 1
assert len(db.rows[VectorFeature]) == 1
def test_governed_lambert_geojson_persists_canonical_consumption_bytes_and_provenance_evidence(
monkeypatch, tmp_path: Path
) -> None:
"""Projected source bytes must never be the file that vector operations consume."""
project = Project(id=uuid4(), name="Canonical GeoJSON storage")
db = _Session(project)
raw, (longitude, latitude, max_longitude, max_latitude) = _lambert_grb_payload()
consumption_path = tmp_path / "consumption" / "grb-buildings.geojson"
provenance_path = tmp_path / "provenance" / "grb-buildings.geojson"
def _persist_dataset_file(**kwargs):
stored = kwargs["content"]
consumption_path.parent.mkdir(parents=True, exist_ok=True)
consumption_path.write_bytes(stored)
return _storage_info(consumption_path.parent, stored)
def _persist_file(storage_path, content, original_filename, content_type):
del storage_path, original_filename, content_type
provenance_path.parent.mkdir(parents=True, exist_ok=True)
provenance_path.write_bytes(content)
return _storage_info(provenance_path.parent, content)
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
_persist_dataset_file,
)
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_file",
_persist_file,
)
result = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="grb-lambert.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={"adapter": "test"},
temporal_series_key="grb:lambert:2026-08",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01-lambert",
temporal_granularity="snapshot",
)
dataset = next(item for item in db.rows[Dataset] if item.id == result.id)
dataset_version = db.rows[DatasetVersion][0]
snapshot = db.rows[SourceSnapshot][0]
canonical_bytes = Path(str(dataset.storage_path)).read_bytes()
canonical_payload = json.loads(canonical_bytes)
source_artifact = dataset.provenance_metadata["source_artifact"]
assert result.status == "ready"
assert dataset.crs == "EPSG:4326"
assert canonical_payload["crs"]["properties"]["name"] == "EPSG:4326"
assert canonical_payload["features"][0]["geometry"]["coordinates"][0][0] == pytest.approx(
[longitude, latitude], abs=0.000001
)
assert sha256(canonical_bytes).hexdigest() == dataset.checksum_sha256
assert dataset_version.checksum_sha256 == dataset.checksum_sha256
assert snapshot.checksum_sha256 == dataset.checksum_sha256
assert source_artifact["retention"] == "provenance_evidence_only"
assert source_artifact["checksum_sha256"] == sha256(raw).hexdigest()
assert source_artifact["storage_path"] != dataset.storage_path
assert Path(source_artifact["storage_path"]).read_bytes() == raw
assert dataset.provenance_metadata["canonical_consumption_artifact"] == {
"checksum_sha256": dataset.checksum_sha256,
"crs": "EPSG:4326",
"storage_role": "dataset_consumption",
}
inspection = VectorOperationsService.inspect(db, dataset.id)
assert inspection.crs == "EPSG:4326"
assert inspection.bounds_json == {
"min_x": pytest.approx(longitude, abs=0.000001),
"min_y": pytest.approx(latitude, abs=0.000001),
"max_x": pytest.approx(max_longitude, abs=0.000001),
"max_y": pytest.approx(max_latitude, abs=0.000001),
}
response_payload = DatasetService.get_dataset_geojson(db, dataset.id)
assert response_payload["features"][0]["geometry"]["coordinates"][0][0] == pytest.approx(
[longitude, latitude], abs=0.000001
)
# The storage identity is enforced at the operation boundary too; a
# replacement with different canonical bytes is not silently processed.
Path(str(dataset.storage_path)).write_bytes(canonical_bytes + b"\n")
with pytest.raises(AppError) as exc_info:
VectorOperationsService.inspect(db, dataset.id)
assert exc_info.value.code == "DATASET_STORAGE_CHECKSUM_MISMATCH"
def test_metadata_refresh_refuses_mutated_governed_artifact(
monkeypatch, tmp_path: Path
) -> None:
"""A passed snapshot cannot be silently re-described from mutable storage."""
project = Project(id=uuid4(), name="Phase 2 immutable refresh")
db = _Session(project)
raw = _valid_payload()
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
lambda **_kwargs: _storage_info(tmp_path, raw),
)
result = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="grb-buildings.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={"adapter": "test"},
temporal_series_key="grb:2026-08",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01",
temporal_granularity="snapshot",
)
dataset = next(item for item in db.rows[Dataset] if item.id == result.id)
original_checksum = dataset.checksum_sha256
original_metadata = dict(dataset.metadata_json or {})
original_commit_count = db.commits
# Simulate an out-of-band storage replacement at the same path. The
# refresh endpoint must not parse it into an already-passed contract row.
Path(str(dataset.storage_path)).write_bytes(_grb_payload_without_required_id())
with pytest.raises(AppError) as exc_info:
DatasetService.refresh_metadata(db, dataset.id)
assert exc_info.value.code == "GOVERNED_DATASET_REINGEST_REQUIRED"
assert dataset.status == "ready"
assert dataset.validation_status == "passed"
assert dataset.checksum_sha256 == original_checksum
assert dataset.metadata_json == original_metadata
assert db.commits == original_commit_count
def test_governed_import_quarantines_bad_artifacts_and_refuses_unknown_source(
monkeypatch, tmp_path: Path
) -> None:
project = Project(id=uuid4(), name="Phase 2 quarantine")
db = _Session(project)
raw = b'{"type":"FeatureCollection","features":[]}'
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
lambda **_kwargs: _storage_info(tmp_path, raw),
)
quarantined = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="empty.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={},
temporal_series_key="grb:2026-08-empty",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01-empty",
temporal_granularity="snapshot",
)
assert quarantined.status == "quarantined"
assert quarantined.validation_status == "failed"
assert quarantined.quarantine_status == "quarantined"
assert len(db.rows[DatasetQuarantine]) == 1
assert db.rows[SourceSnapshot][0].ingest_status == "quarantined"
with pytest.raises(AppError) as exc_info:
DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="unregistered.geojson",
content=_valid_payload(),
source="caller_controlled",
source_name="caller_claimed_grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={},
)
assert exc_info.value.code == "SOURCE_REGISTRY_ENTRY_NOT_FOUND"
def test_governed_grb_vector_quarantines_missing_server_owned_required_attribute(
monkeypatch, tmp_path: Path
) -> None:
project = Project(id=uuid4(), name="Phase 2 source schema")
db = _Session(project)
raw = _grb_payload_without_required_id()
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file",
lambda **kwargs: _storage_info(tmp_path, kwargs["content"]),
)
quarantined = DatasetService.import_vector_bytes(
db,
project_id=project.id,
filename="grb-missing-id.geojson",
content=raw,
source="grb_wfs",
source_name="grb",
dataset_role="reference",
reference_layer_name="buildings",
source_metadata={"license": "Open data"},
provenance_metadata={"adapter": "test"},
temporal_series_key="grb:missing-id",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01-missing-id",
temporal_granularity="snapshot",
)
dataset = next(item for item in db.rows[Dataset] if item.id == quarantined.id)
assert quarantined.status == "quarantined"
assert dataset.validation_status == "failed"
assert dataset.quarantine_status == "quarantined"
issue = dataset.validation_report_json["issues"][0]
assert issue["code"] == "SOURCE_SCHEMA_REQUIRED_ATTRIBUTE_MISSING"
assert issue["category"] == "source_schema"
assert len(db.rows[DatasetQuarantine]) == 1
def test_partitioned_vector_ingest_is_idempotent_and_quarantines_noncanonical_partition_coordinates(
monkeypatch,
tmp_path: Path,
) -> None:
project = Project(id=uuid4(), name="Partitioned governed ingest")
area = Area(id=uuid4(), project_id=project.id, name="Partitioned AOI")
db = _Session(project)
db.rows[Area] = [area]
feature = {
"type": "Feature",
"id": "GBG.1",
"properties": {"id": "GBG.1", "source_feature_id": "GBG.1"},
"geometry": {
"type": "Polygon",
"coordinates": [
[[4.69, 51.09], [4.70, 51.09], [4.70, 51.10], [4.69, 51.09]]
],
},
}
partition_payload = {"type": "FeatureCollection", "features": [feature]}
partition_path = tmp_path / "partition-01.geojson"
partition_path.write_text(json.dumps(partition_payload), encoding="utf-8")
artifact_payload = {
"type": "FeatureCollection",
"crs": "EPSG:4326",
"features": [feature],
}
artifact_path = tmp_path / "grb-partitioned.geojson"
artifact_raw = json.dumps(artifact_payload).encode("utf-8")
artifact_path.write_bytes(artifact_raw)
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file_from_path",
lambda **_kwargs: _storage_info(tmp_path, artifact_raw),
)
result = DatasetService.import_partitioned_vector_artifact(
db,
project_id=project.id,
area_id=area.id,
artifact_path=artifact_path,
partition_paths=[partition_path],
original_filename="grb-partitioned.geojson",
source="operator_official_import",
dataset_role="reference",
source_name="grb",
reference_layer_name="buildings",
metadata_json={
"feature_count": 1,
"crs": "EPSG:4326",
"bounds_json": {
"min_x": 4.69,
"min_y": 51.09,
"max_x": 4.70,
"max_y": 51.10,
},
},
source_metadata={"license": "Open data"},
provenance_metadata={
"artifact_sha256": sha256(artifact_raw).hexdigest(),
"partition_checksums": {
partition_path.name: sha256(partition_path.read_bytes()).hexdigest()
},
},
temporal_series_key="grb:partitioned:test",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01",
)
dataset = next(item for item in db.rows[Dataset] if item.id == result.id)
assert result.status == "ready"
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.source_name == "grb"
assert len(db.rows[SourceSnapshot]) == 1
assert len(db.rows[VectorFeature]) == 1
assert dataset.metadata_json["partitioned_geometry_audit"][
"partition_checksums_sha256"
] == {partition_path.name: sha256(partition_path.read_bytes()).hexdigest()}
assert dataset.provenance_metadata["partition_checksum_manifest_sha256"]
assert dataset.provenance_metadata["partitioned_artifact_binding_sha256"]
repeated = DatasetService.import_partitioned_vector_artifact(
db,
project_id=project.id,
area_id=area.id,
artifact_path=artifact_path,
partition_paths=[partition_path],
original_filename="grb-partitioned.geojson",
source="operator_official_import",
dataset_role="reference",
source_name="grb",
reference_layer_name="buildings",
metadata_json={
"feature_count": 1,
"crs": "EPSG:4326",
"bounds_json": {
"min_x": 4.69,
"min_y": 51.09,
"max_x": 4.70,
"max_y": 51.10,
},
},
source_metadata={"license": "Open data"},
provenance_metadata={
"artifact_sha256": sha256(artifact_raw).hexdigest(),
"partition_checksums": {
partition_path.name: sha256(partition_path.read_bytes()).hexdigest()
},
},
temporal_series_key="grb:partitioned:test",
observed_at=datetime(2026, 8, 1, tzinfo=UTC),
source_version="2026-08-01",
)
assert repeated.id == result.id
assert len(db.rows[Dataset]) == 1
assert len(db.rows[VectorFeature]) == 1
lambert_feature = {
**feature,
"id": "GBG.lambert",
"properties": {"id": "GBG.lambert"},
"geometry": {
"type": "Polygon",
"coordinates": [
[[150000, 170000], [150010, 170000], [150010, 170010], [150000, 170000]]
],
},
}
lambert_partition = tmp_path / "partition-lambert.geojson"
lambert_partition.write_text(
json.dumps({"type": "FeatureCollection", "features": [lambert_feature]}),
encoding="utf-8",
)
lambert_artifact = tmp_path / "grb-lambert.geojson"
lambert_raw = json.dumps(
{"type": "FeatureCollection", "features": [lambert_feature]}
).encode("utf-8")
lambert_artifact.write_bytes(lambert_raw)
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file_from_path",
lambda **_kwargs: _storage_info(tmp_path, lambert_raw),
)
quarantined = DatasetService.import_partitioned_vector_artifact(
db,
project_id=project.id,
area_id=area.id,
artifact_path=lambert_artifact,
partition_paths=[lambert_partition],
original_filename="grb-lambert.geojson",
source="operator_official_import",
dataset_role="reference",
source_name="grb",
reference_layer_name="buildings",
metadata_json={
"feature_count": 1,
"crs": "EPSG:4326",
"bounds_json": {
"min_x": 150000,
"min_y": 170000,
"max_x": 150010,
"max_y": 170010,
},
},
source_metadata={"license": "Open data"},
provenance_metadata={
"artifact_sha256": sha256(lambert_raw).hexdigest(),
"partition_checksums": {
lambert_partition.name: sha256(
lambert_partition.read_bytes()
).hexdigest()
},
},
temporal_series_key="grb:partitioned:lambert",
observed_at=datetime(2026, 8, 2, tzinfo=UTC),
source_version="2026-08-02",
)
assert quarantined.status == "quarantined"
assert quarantined.validation_status == "failed"
assert quarantined.quarantine_status == "quarantined"
missing_manifest_partition = tmp_path / "partition-missing-manifest.geojson"
missing_manifest_partition.write_text(
json.dumps(partition_payload), encoding="utf-8"
)
missing_manifest_artifact = tmp_path / "grb-missing-manifest.geojson"
missing_manifest_raw = json.dumps(
{"type": "FeatureCollection", "features": [feature]}
).encode("utf-8")
missing_manifest_artifact.write_bytes(missing_manifest_raw)
monkeypatch.setattr(
"app.services.dataset_service.StorageService.persist_dataset_file_from_path",
lambda **_kwargs: _storage_info(tmp_path, missing_manifest_raw),
)
missing_manifest = DatasetService.import_partitioned_vector_artifact(
db,
project_id=project.id,
area_id=area.id,
artifact_path=missing_manifest_artifact,
partition_paths=[missing_manifest_partition],
original_filename="grb-missing-manifest.geojson",
source="operator_official_import",
dataset_role="reference",
source_name="grb",
reference_layer_name="buildings",
metadata_json={
"feature_count": 1,
"crs": "EPSG:4326",
"bounds_json": {
"min_x": 4.69,
"min_y": 51.09,
"max_x": 4.70,
"max_y": 51.10,
},
},
source_metadata={"license": "Open data"},
provenance_metadata={
"artifact_sha256": sha256(missing_manifest_raw).hexdigest()
},
temporal_series_key="grb:partitioned:missing-manifest",
observed_at=datetime(2026, 8, 3, tzinfo=UTC),
source_version="2026-08-03",
)
assert missing_manifest.status == "quarantined"
assert (
missing_manifest.validation_report_json["issues"][0]["code"]
== "PARTITION_CHECKSUM_MANIFEST_REQUIRED"
)
def test_partitioned_geometry_audit_handles_more_than_generic_topology_limit_without_materializing_geometries(
tmp_path: Path,
) -> None:
feature_count = 10_001
partition_path = tmp_path / "large-partition.geojson"
partition_path.write_text(
json.dumps(
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": f"GBG.{index}",
"properties": {"id": f"GBG.{index}"},
"geometry": {
"type": "Point",
"coordinates": [4.0 + index / 10_000_000, 51.0],
},
}
for index in range(feature_count)
],
}
),
encoding="utf-8",
)
audit = _PartitionedGeoJsonRecords(
[partition_path],
expected_feature_count=feature_count,
declared_partition_checksums={
partition_path.name: sha256(partition_path.read_bytes()).hexdigest()
},
).audit()
assert audit.feature_count == feature_count
assert audit.bounds_json["min_x"] == 4.0
assert audit.bounds_json["max_x"] > audit.bounds_json["min_x"]
assert audit.representative_record.geometry.geom_type == "MultiPoint"
+96 -16
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import subprocess
import sys
from hashlib import sha256
from pathlib import Path
from PIL import Image
@@ -13,35 +14,114 @@ SCRIPT = Path(__file__).parents[2] / "scripts" / "build_grayscale_yolo_dataset.p
def test_grayscale_builder_preserves_labels_and_split(tmp_path: Path) -> None:
source = tmp_path / "source"
(source / "images" / "train").mkdir(parents=True)
(source / "labels" / "train").mkdir(parents=True)
image = source / "images" / "train" / "tile.png"
label = source / "labels" / "train" / "tile.txt"
Image.new("RGB", (8, 8), (255, 0, 0)).save(image)
label.write_text("0 0.5 0.5 0.5 0.5\n", encoding="utf-8")
entries = []
for split, sample_slug, colour in (("train", "fixture-train", (255, 0, 0)), ("val", "fixture-val", (0, 255, 0))):
image = source / "images" / split / f"{sample_slug}.png"
label = source / "labels" / split / f"{sample_slug}.txt"
image.parent.mkdir(parents=True, exist_ok=True)
label.parent.mkdir(parents=True, exist_ok=True)
Image.new("RGB", (8, 8), colour).save(image)
label.write_text("0 0.5 0.5 0.5 0.5\n", encoding="utf-8")
entries.append((split, sample_slug, image, label))
manifest = source / "operator_samples_manifest.json"
policy = "geointel-training-source-eligibility/v1"
manifest.write_text(
json.dumps(
{
"immutable": True,
"training_eligibility": {"policy_version": policy, "status": "eligible", "fixture_mode": True},
"samples": [
{
"sample_slug": sample_slug,
"split": split,
"raster_dataset_id": f"raster:{sample_slug}",
"reference_dataset_id": f"reference:{sample_slug}",
"training_eligibility": {
"policy_version": policy,
"eligible": True,
"fixture_mode": True,
"raster": {"eligible": True, "reasons": [], "evidence": {"dataset_id": f"raster:{sample_slug}", "checksum_sha256": "a" * 64, "source_registry_id": "fixture-raster", "source_snapshot_id": "fixture-raster-snapshot"}},
"reference": {"eligible": True, "reasons": [], "evidence": {"dataset_id": f"reference:{sample_slug}", "checksum_sha256": "b" * 64, "source_registry_id": "fixture-reference", "source_snapshot_id": "fixture-reference-snapshot"}},
},
}
for split, sample_slug, _image, _label in entries
],
}
),
encoding="utf-8",
)
(source / "corpus-freeze.json").write_text(
json.dumps(
{
"schema_version": 2,
"immutable": True,
"fixture_mode": True,
"training_eligibility_policy": policy,
"manifest_sha256": sha256(manifest.read_bytes()).hexdigest(),
}
),
encoding="utf-8",
)
dataset_yaml = source / "dataset.yaml"
dataset_yaml.write_text(
f"path: {source}\ntrain: images/train\nval: images/val\nnames:\n 0: building\n",
encoding="utf-8",
)
release_script = SCRIPT.parent / "training_release_manifest.py"
subprocess.run(
[
sys.executable,
str(release_script),
"create",
"--train-yaml",
str(dataset_yaml),
"--corpus-manifest",
str(manifest),
"--fixture-mode",
],
check=True,
)
release_path = dataset_yaml.with_name(dataset_yaml.name + ".geointel-training-release.json")
asset_path = dataset_yaml.with_name(dataset_yaml.name + ".geointel-training-assets.json")
assets = json.loads(asset_path.read_text(encoding="utf-8"))
summary = source / "yolo_tile_dataset_summary.json"
summary.write_text(
json.dumps(
{
"dataset_yaml": str(dataset_yaml.resolve()),
"training_release_manifest": str(release_path.resolve()),
"training_release_manifest_sha256": sha256(release_path.read_bytes()).hexdigest(),
"training_asset_manifest": str(asset_path.resolve()),
"source_manifest_sha256": sha256(manifest.read_bytes()).hexdigest(),
"tiles": [
{
"split": "train",
"image_path": str(image),
"label_path": str(label),
}
]
{"split": entry["split"], "image_path": entry["image_path"], "label_path": entry["label_path"]}
for entry in assets["entries"]
],
}
),
encoding="utf-8",
)
output = tmp_path / "gray"
subprocess.run(
[sys.executable, str(SCRIPT), "--summary", str(summary), "--output-dir", str(output)],
[
sys.executable,
str(SCRIPT),
"--summary",
str(summary),
"--train-yaml",
str(dataset_yaml),
"--corpus-manifest",
str(manifest),
"--fixture-mode",
"--output-dir",
str(output),
],
check=True,
)
converted = Image.open(output / "images" / "train" / "tile.png")
converted = Image.open(output / "images" / "train" / "fixture-train.png")
r, g, b = converted.getpixel((0, 0))
assert r == g == b
assert (output / "labels" / "train" / "tile.txt").read_text() == label.read_text()
assert (output / "labels" / "train" / "fixture-train.txt").read_text() == "0 0.5 0.5 0.5 0.5\n"
evidence = json.loads((output / "grayscale-dataset-evidence.json").read_text())
assert evidence["converted_tile_count"] == 1
assert evidence["converted_tile_count"] == 2
assert evidence["training_eligible"] is False
+103 -2
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from hashlib import sha256
import json
from pathlib import Path
from uuid import uuid4
@@ -10,9 +11,10 @@ from fastapi.testclient import TestClient
from app.core.config import Settings
from app.core.errors import AppError
from app.main import app
from app.models import AnalysisRun, Dataset, Detection, Job, Project
from app.models import AnalysisRun, Dataset, Detection, Job, Project, SourceRegistry, SourceSnapshot
from app.services.detection_service import DetectionService
from app.services.model_asset_catalog_service import ModelAssetCatalogService
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
class FakeSession:
@@ -63,15 +65,47 @@ class MockYoloAdapter:
def _project_and_raster_dataset():
project_id = uuid4()
dataset_id = uuid4()
source_registry_id = uuid4()
source_snapshot_id = uuid4()
checksum = "a" * 64
project = Project(id=project_id, name="Geel")
source_registry = SourceRegistry(
id=source_registry_id,
source_key="test-derived-raster",
display_name="Governed test-derived raster",
classification="derived",
authority_name="GeoIntel test fixture",
usage_policy_json={"ground_truth_allowed": False},
)
source_snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key="test-derived-raster-v1",
checksum_sha256=checksum,
freshness_status="current",
ingest_status="ingested",
)
dataset = Dataset(
id=dataset_id,
project_id=project_id,
name="source.tif",
dataset_type="raster",
source="user_upload",
source="test-derived-raster",
source_name="test-derived-raster",
storage_path="storage/uploads/source.tif",
checksum_sha256=checksum,
source_registry_id=source_registry_id,
source_snapshot_id=source_snapshot_id,
data_contract_key="geointel.raster.geotiff",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
status="ready",
)
dataset.source_registry = source_registry
dataset.source_snapshot = source_snapshot
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
return db, project_id, dataset_id
@@ -101,6 +135,72 @@ def _manifest(tmp_path: Path) -> Path:
return manifest_path
def _write_model_sidecar(
model_path: Path,
settings: Settings,
*,
db: FakeSession | None = None,
) -> None:
model_sha256 = sha256(model_path.read_bytes()).hexdigest()
source_registry_id = uuid4()
source_snapshot_id = uuid4()
source_version = settings.yolo_model_version or "test-v1"
if db is not None:
source_registry = SourceRegistry(
id=source_registry_id,
source_key="model",
display_name="Governed test model artifact",
classification="experimental",
authority_name="GeoIntel test fixture",
freshness_status="current",
ingest_status="configured",
)
source_snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key=f"model-{source_version}",
source_version=source_version,
checksum_sha256=model_sha256,
freshness_status="current",
ingest_status="ingested",
)
db.objects[(SourceRegistry, source_registry_id)] = source_registry
db.objects[(SourceSnapshot, source_snapshot_id)] = source_snapshot
payload = {
"schema_version": RuntimeModelProvenanceService.MANIFEST_SCHEMA_VERSION,
"data_contract": {"key": "geointel.model.pytorch", "version": "1.0.0"},
"model": {
"model_id": settings.yolo_model_id,
"task_type": "object_detection",
"sha256": model_sha256,
"model_format": "pytorch",
"framework": "ultralytics/pytorch",
"class_mapping": {"0": "building"},
"source_version": source_version,
},
"source": {
"source_registry_id": str(source_registry_id),
"source_snapshot_id": str(source_snapshot_id),
"source_registry_key": "model",
"source_snapshot_checksum_sha256": model_sha256,
},
"lineage": {
"upstream_asset_ids": ["test-training-corpus"],
"upstream_checksums_sha256": ["a" * 64],
"transformations": [
{"name": "test-training", "version": "1.0.0", "checksum_sha256": "b" * 64}
],
},
"metadata": {"training_manifest_sha256": "c" * 64},
"imported_at": "2026-08-01T10:00:00+00:00",
}
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
RuntimeModelProvenanceService.manifest_path_for_model(model_path).write_text(
json.dumps(payload, sort_keys=True),
encoding="utf-8",
)
def test_model_asset_catalog_lists_supported_local_model_files(tmp_path: Path) -> None:
model_file = tmp_path / "building-detector.pt"
model_file.write_bytes(b"local model")
@@ -190,6 +290,7 @@ def test_detection_run_persists_selected_model_asset_parameters(tmp_path: Path)
yolo_models_dir=str(tmp_path),
yolo_max_tiles=4,
)
_write_model_sidecar(model_file, settings, db=db)
result = DetectionService.run_detection(
db=db,
@@ -0,0 +1,644 @@
from __future__ import annotations
from datetime import datetime, timezone
from hashlib import sha256
import json
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
from app.models import Dataset, DatasetVersion, SourceRegistry, SourceSnapshot
from app.services.data_contract_validation import (
build_vector_ingest_input,
validate_registered_asset,
)
from app.services.demo_workflow_service import DemoWorkflowService
from app.services.derived_dataset_governance_service import (
DerivedDatasetGovernanceService,
)
from app.services.raster_operations_service import RasterOperationsService
from app.services.storage_service import StorageService
from app.services.vector_feature_service import VectorFeatureService
from app.services.vector_operations_service import VectorOperationsService
_CHECKSUM = "a" * 64
class _FakeSession:
def __init__(self, rows=None) -> None:
self.rows = rows or {}
self.added = []
def get(self, model, row_id):
row = self.rows.get((model, row_id))
if row is not None:
return row
return next(
(
item
for item in self.added
if isinstance(item, model) and item.id == row_id
),
None,
)
def add(self, row) -> None:
self.added.append(row)
def commit(self) -> None:
return None
def refresh(self, row) -> None:
return None
class _GovernedSession:
"""Small ORM-shaped session for the real governance branch.
Registry persistence is monkeypatched below; the test exercises the
service's orchestration and report decisions without needing PostGIS.
"""
def __init__(self) -> None:
self.added = []
self.flushes = 0
class _EmptyQuery:
def filter(self, *_args, **_kwargs):
return self
@staticmethod
def all():
return []
@staticmethod
def one_or_none():
return None
def query(self, *_args, **_kwargs):
# Governance now performs a bounded lineage traversal during quarantine.
# This focused harness intentionally has no persisted siblings/edges.
return self._EmptyQuery()
def add(self, row) -> None:
self.added.append(row)
def flush(self) -> None:
self.flushes += 1
def _governed_parent() -> Dataset:
source_id = uuid4()
snapshot_id = uuid4()
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB parent fixture",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key="governed-parent",
checksum_sha256=_CHECKSUM,
freshness_status="current",
ingest_status="ingested",
)
dataset = Dataset(
id=uuid4(),
project_id=uuid4(),
name="governed.geojson",
dataset_type="vector",
source="grb",
source_name="grb",
status="ready",
checksum_sha256=_CHECKSUM,
data_contract_key="geointel.vector.geojson",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
source_registry_id=source_id,
source_snapshot_id=snapshot_id,
)
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_lineage_evidence_quarantines_ungoverned_parent_without_inventing_a_checksum() -> (
None
):
parent = _governed_parent()
valid_lineage = DerivedDatasetGovernanceService._lineage_evidence(
parent, "vector.clip", {"area_id": "a"}
)
valid_report = validate_registered_asset(
build_vector_ingest_input(
asset_id="derived-valid",
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
feature_collection={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
}
],
},
checksum_sha256=_CHECKSUM,
computed_checksum_sha256=_CHECKSUM,
source_registry_id="derived-source",
source_snapshot_id="derived-snapshot",
imported_at=datetime.now(timezone.utc),
metadata={
"license": "internal derived artifact",
"bounds_json": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.0,
"max_y": 51.0,
},
},
temporal_unknown_reason="derived input has no precise observation timestamp",
source_version_unknown_reason="transform version is recorded separately",
lineage=valid_lineage,
)
)
assert valid_report.validation_status.value == "passed"
assert valid_lineage.upstream_asset_ids == (str(parent.id),)
assert valid_lineage.upstream_checksums_sha256 == (_CHECKSUM,)
parent.validation_status = "not_validated"
rejected_lineage = DerivedDatasetGovernanceService._lineage_evidence(
parent, "vector.clip", {}
)
rejected_report = validate_registered_asset(
build_vector_ingest_input(
asset_id="derived-rejected",
source_crs="EPSG:4326",
storage_crs="EPSG:4326",
feature_collection={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
}
],
},
checksum_sha256=_CHECKSUM,
computed_checksum_sha256=_CHECKSUM,
source_registry_id="derived-source",
source_snapshot_id="derived-snapshot",
imported_at=datetime.now(timezone.utc),
metadata={
"license": "internal derived artifact",
"bounds_json": {
"min_x": 5.0,
"min_y": 51.0,
"max_x": 5.0,
"max_y": 51.0,
},
},
temporal_unknown_reason="derived input has no precise observation timestamp",
source_version_unknown_reason="transform version is recorded separately",
lineage=rejected_lineage,
)
)
assert rejected_lineage.upstream_checksums_sha256 == (
"parent_dataset_not_governed",
)
assert rejected_report.validation_status.value == "failed"
assert rejected_report.quarantine_status.value == "quarantined"
assert any(
issue.code == "UPSTREAM_CHECKSUM_FORMAT_INVALID"
for issue in rejected_report.issues
)
def test_govern_vector_binds_snapshot_contract_and_lineage_before_marking_ready(
monkeypatch,
) -> None:
from app.services.source_registry_service import SourceRegistryService
db = _GovernedSession()
parent = _governed_parent()
parent_version_id = uuid4()
dataset = Dataset(
id=uuid4(),
project_id=parent.project_id,
name="derived.geojson",
dataset_type="vector",
source="operation:clip",
source_name="derived",
checksum_sha256=_CHECKSUM,
imported_at=datetime.now(timezone.utc),
crs="EPSG:4326",
metadata_json={
"bounds_json": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.0, "max_y": 51.0}
},
status="validating",
)
version = DatasetVersion(
id=uuid4(), dataset_id=dataset.id, version=1, checksum_sha256=_CHECKSUM
)
source = SimpleNamespace(id=uuid4(), license_name="internal derived artifact")
snapshot = SimpleNamespace(id=uuid4(), source_registry_id=source.id)
edges = []
monkeypatch.setattr(
SourceRegistryService,
"ensure_server_owned_source",
lambda *_args, **_kwargs: source,
)
monkeypatch.setattr(
SourceRegistryService, "record_snapshot", lambda *_args, **_kwargs: snapshot
)
monkeypatch.setattr(
DerivedDatasetGovernanceService,
"_latest_parent_version_id",
lambda *_args: parent_version_id,
)
def _bind(target, **kwargs):
target.source_registry_id = kwargs["source"].id
target.source_snapshot_id = kwargs["snapshot"].id
target.data_contract_key = kwargs["data_contract_key"]
target.data_contract_version = kwargs["data_contract_version"]
target.validation_status = kwargs["validation_status"]
target.provenance_status = kwargs["provenance_status"]
target.lineage_status = kwargs["lineage_status"]
return target
monkeypatch.setattr(SourceRegistryService, "bind_dataset_provenance", _bind)
monkeypatch.setattr(SourceRegistryService, "bind_dataset_version_provenance", _bind)
monkeypatch.setattr(
SourceRegistryService,
"record_lineage_edge",
lambda *_args, **kwargs: edges.append(kwargs),
)
ready = DerivedDatasetGovernanceService.govern_vector(
db,
dataset=dataset,
dataset_version=version,
feature_collection={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
}
],
},
source_key="derived",
operation="vector.clip",
parent_dataset=parent,
operation_parameters={"area_id": "a"},
)
assert ready is True
assert dataset.status == "ready"
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.source_registry_id == source.id
assert version.source_snapshot_id == snapshot.id
assert db.flushes >= 1
assert edges[0]["parent_dataset_id"] == parent.id
assert edges[0]["parent_dataset_version_id"] == parent_version_id
assert edges[0]["child_dataset_version_id"] == version.id
def test_govern_vector_quarantines_output_when_parent_is_manual_or_experimental(
monkeypatch,
) -> None:
from app.services.source_registry_service import SourceRegistryService
db = _GovernedSession()
parent = _governed_parent()
parent.source = "manual"
parent.source_name = "manual"
parent.source_registry.source_key = "manual"
parent.source_registry.classification = "experimental"
parent.source_registry.usage_policy_json = {"ground_truth_allowed": False}
dataset = Dataset(
id=uuid4(),
project_id=parent.project_id,
name="manual-derived.geojson",
dataset_type="vector",
source="operation:clip",
source_name="derived",
checksum_sha256=_CHECKSUM,
imported_at=datetime.now(timezone.utc),
crs="EPSG:4326",
metadata_json={
"bounds_json": {"min_x": 5.0, "min_y": 51.0, "max_x": 5.0, "max_y": 51.0}
},
status="validating",
)
version = DatasetVersion(
id=uuid4(), dataset_id=dataset.id, version=1, checksum_sha256=_CHECKSUM
)
source = SimpleNamespace(id=uuid4(), license_name="internal derived artifact")
snapshot = SimpleNamespace(
id=uuid4(), source_registry_id=source.id, ingest_status="ingested"
)
monkeypatch.setattr(
SourceRegistryService,
"ensure_server_owned_source",
lambda *_args, **_kwargs: source,
)
monkeypatch.setattr(
SourceRegistryService, "record_snapshot", lambda *_args, **_kwargs: snapshot
)
monkeypatch.setattr(
DerivedDatasetGovernanceService,
"_latest_parent_version_id",
lambda *_args: None,
)
monkeypatch.setattr(
SourceRegistryService, "record_lineage_edge", lambda *_args, **_kwargs: None
)
ready = DerivedDatasetGovernanceService.govern_vector(
db,
dataset=dataset,
dataset_version=version,
feature_collection={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
}
],
},
source_key="derived",
operation="vector.clip",
parent_dataset=parent,
)
assert ready is False
assert dataset.status == "quarantined"
assert dataset.quarantine_status == "quarantined"
assert dataset.validation_status == "failed"
assert any(
issue["code"] == "PARENT_DATASET_NOT_ELIGIBLE_FOR_DERIVED_PROCESSING"
for issue in dataset.validation_report_json["issues"]
)
assert snapshot.ingest_status == "quarantined"
def test_vector_selection_uses_map_selection_registry_and_skips_features_when_quarantined(
monkeypatch, tmp_path
) -> None:
source = _governed_parent()
source.area_id = None
source.storage_path = str(tmp_path / "source.geojson")
db = _FakeSession({(Dataset, source.id): source})
output_path = tmp_path / "selection.geojson"
calls = []
persisted_features = []
monkeypatch.setattr(
VectorFeatureService,
"select_features_by_bbox",
lambda *_args, **_kwargs: {
"selection_bbox": {
"min_x": 4.9,
"min_y": 50.9,
"max_x": 5.2,
"max_y": 51.2,
"crs": "EPSG:4326",
},
"feature_count": 1,
"limit": 250,
"truncated": False,
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "source-feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {
"vector_feature_id": "source-feature",
"dataset_id": str(source.id),
},
}
],
},
},
)
def _persist_dataset_file(**kwargs):
output_path.write_bytes(kwargs["content"])
return {
"original_filename": kwargs["original_filename"],
"stored_filename": output_path.name,
"content_type": kwargs["content_type"],
"size_bytes": len(kwargs["content"]),
"checksum_sha256": _CHECKSUM,
"storage_path": str(output_path),
}
monkeypatch.setattr(StorageService, "persist_dataset_file", _persist_dataset_file)
def _quarantine(db, **kwargs):
calls.append(kwargs)
kwargs["dataset"].status = "quarantined"
return False
monkeypatch.setattr(DerivedDatasetGovernanceService, "govern_vector", _quarantine)
monkeypatch.setattr(
VectorFeatureService,
"persist_geojson_features",
lambda **kwargs: persisted_features.append(kwargs),
)
response = VectorOperationsService.derive_selection_dataset(
db=db,
dataset_id=source.id,
bbox={
"min_x": 4.9,
"min_y": 50.9,
"max_x": 5.2,
"max_y": 51.2,
"crs": "EPSG:4326",
},
)
assert response.status == "quarantined"
assert calls[0]["source_key"] == "map_selection"
assert calls[0]["parent_dataset"] is source
assert calls[0]["operation"] == "vector.selection"
assert persisted_features == []
def test_vector_buffer_uses_projected_metres_instead_of_wgs84_degrees(
monkeypatch, tmp_path
) -> None:
source = _governed_parent()
source.storage_path = str(tmp_path / "source.geojson")
source.crs = "EPSG:4326"
Path(source.storage_path).write_text(
json.dumps(
{
"type": "FeatureCollection",
"crs": "EPSG:4326",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
},
],
}
),
encoding="utf-8",
)
# A governed consumption artifact must carry the checksum of these exact
# bytes; vector operations deliberately refuse a stale fixture checksum.
source.checksum_sha256 = sha256(Path(source.storage_path).read_bytes()).hexdigest()
source.source_snapshot.checksum_sha256 = source.checksum_sha256
db = _FakeSession({(Dataset, source.id): source})
captured = {}
def _persist(**kwargs):
captured.update(kwargs)
return uuid4()
monkeypatch.setattr(VectorOperationsService, "_persist_derived_dataset", _persist)
VectorOperationsService.buffer(
db, source.id, distance_m=100.0, dissolve=False, output_name=None
)
coordinates = captured["feature_collection"]["features"][0]["geometry"][
"coordinates"
][0]
longitudes = [coordinate[0] for coordinate in coordinates]
latitudes = [coordinate[1] for coordinate in coordinates]
assert max(longitudes) - min(longitudes) < 0.01
assert max(latitudes) - min(latitudes) < 0.01
def test_raster_operation_uses_derived_registry_before_commit(
monkeypatch, tmp_path
) -> None:
source = _governed_parent()
source.dataset_type = "raster"
source.storage_path = str(tmp_path / "source.tif")
output_path = tmp_path / "derived.tif"
output_path.write_bytes(b"derived-raster")
db = _FakeSession()
calls = []
def _govern(db, **kwargs):
calls.append(kwargs)
kwargs["dataset"].status = "quarantined"
return False
monkeypatch.setattr(DerivedDatasetGovernanceService, "govern_raster", _govern)
result = RasterOperationsService._persist_derived_dataset(
db,
source_dataset=source,
source_dataset_id=source.id,
operation="ndvi",
output_path=str(output_path),
output_name="derived.tif",
metadata={
"crs": "EPSG:31370",
"bounds": [100000.0, 100000.0, 100001.0, 100001.0],
"resolution": [1.0, 1.0],
"width": 1,
"height": 1,
"band_count": 1,
"dtype": ["float32"],
"operation_parameters": {"nir_band": 4, "red_band": 3},
},
)
derived = next(item for item in db.added if isinstance(item, Dataset))
assert result == derived.id
assert derived.status == "quarantined"
assert derived.source_name == "derived"
assert calls[0]["source_key"] == "derived"
assert calls[0]["parent_dataset"] is source
assert calls[0]["operation"] == "raster.ndvi"
def test_demo_fixture_creation_is_governed_and_does_not_persist_features_when_rejected(
monkeypatch, tmp_path
) -> None:
project_id = uuid4()
area_id = uuid4()
db = _FakeSession()
calls = []
features = []
monkeypatch.setattr(
StorageService,
"persist_dataset_file",
lambda **kwargs: {
"storage_path": str(tmp_path / kwargs["original_filename"]),
"original_filename": kwargs["original_filename"],
"stored_filename": kwargs["original_filename"],
"content_type": kwargs["content_type"],
"size_bytes": len(kwargs["content"]),
"checksum_sha256": _CHECKSUM,
},
)
def _quarantine(db, **kwargs):
calls.append(kwargs)
kwargs["dataset"].status = "quarantined"
return False
monkeypatch.setattr(DerivedDatasetGovernanceService, "govern_vector", _quarantine)
monkeypatch.setattr(
VectorFeatureService,
"persist_geojson_features",
lambda **kwargs: features.append(kwargs),
)
payload = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5.0, 51.0]},
"properties": {},
}
],
}
dataset = DemoWorkflowService._create_dataset(
db,
project_id=project_id,
area_id=area_id,
filename="fixture.geojson",
payload=payload,
raw=json.dumps(payload).encode("utf-8"),
role="source",
source_name="fixture",
reference_layer_name=None,
)
assert dataset.status == "quarantined"
assert calls[0]["source_key"] == "fixture"
assert calls[0]["operation"] == "demo.fixture_vector"
assert features == []
+47 -6
View File
@@ -1,11 +1,11 @@
from __future__ import annotations
import json
from hashlib import sha256
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
from app.models import Area, Dataset
from app.models import Dataset, SourceRegistry, SourceSnapshot
from app.services.qa_service import QaService
@@ -46,6 +46,47 @@ def _write_features(path: Path, features: list[dict]) -> None:
path.write_text(json.dumps({"type": "FeatureCollection", "features": features}), encoding="utf-8")
def _authoritative_reference(dataset: Dataset) -> Dataset:
"""Give QA reference fixtures the same durable authority proof as GRB."""
source_id = uuid4()
snapshot_id = uuid4()
checksum = sha256(Path(str(dataset.storage_path)).read_bytes()).hexdigest()
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB test reference",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True, "validation_authority": {"building_validation": "primary"}},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key=f"qa-grb-{dataset.id}",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset.source = "grb"
dataset.source_name = "grb"
dataset.dataset_role = "reference"
dataset.status = "ready"
dataset.checksum_sha256 = checksum
dataset.source_registry_id = source_id
dataset.source_snapshot_id = snapshot_id
dataset.data_contract_key = "geointel.vector.geojson"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "complete"
dataset.quarantine_status = "not_quarantined"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_qa_compare_candidate_with_reference_returns_metrics(tmp_path) -> None:
project_id = uuid4()
candidate_id = uuid4()
@@ -66,7 +107,7 @@ def test_qa_compare_candidate_with_reference_returns_metrics(tmp_path) -> None:
crs="EPSG:4326",
metadata_json={"crs_assumed": False},
)
reference = Dataset(
reference = _authoritative_reference(Dataset(
id=reference_id,
project_id=project_id,
name="reference.geojson",
@@ -75,7 +116,7 @@ def test_qa_compare_candidate_with_reference_returns_metrics(tmp_path) -> None:
storage_path=str(reference_path),
crs="EPSG:4326",
metadata_json={"crs_assumed": False},
)
))
result = QaService.compare_candidate_with_reference(
db=FakeSession([candidate, reference]),
@@ -117,7 +158,7 @@ def test_qa_compare_candidate_with_reference_returns_feature_level_evidence(tmp_
crs="EPSG:4326",
metadata_json={"crs_assumed": False},
)
reference = Dataset(
reference = _authoritative_reference(Dataset(
id=reference_id,
project_id=project_id,
name="reference.geojson",
@@ -126,7 +167,7 @@ def test_qa_compare_candidate_with_reference_returns_feature_level_evidence(tmp_
storage_path=str(reference_path),
crs="EPSG:4326",
metadata_json={"crs_assumed": False},
)
))
result = QaService.compare_candidate_with_reference(
db=FakeSession([candidate, reference]),
+80 -24
View File
@@ -49,6 +49,54 @@ def scope_area(name: str, geometry):
return SimpleNamespace(name=name, geometry=geometry)
def governed_materialization(
*,
source_name: str,
reference_layer_name: str | None,
source_metadata: dict[str, object],
dataset_id=None,
) -> SimpleNamespace:
"""Build a complete authoritative materialization for coverage tests.
Coverage is a production-facing statement. These fixtures must therefore
carry the same registry, immutable snapshot, checksum and freshness state
that a materialized official dataset needs in production.
"""
source_registry_id = uuid4()
source_snapshot_id = uuid4()
checksum_sha256 = "a" * 64
return SimpleNamespace(
id=dataset_id or uuid4(),
status="ready",
source=source_name,
source_name=source_name,
reference_layer_name=reference_layer_name,
source_metadata=dict(source_metadata),
checksum_sha256=checksum_sha256,
source_registry_id=source_registry_id,
source_snapshot_id=source_snapshot_id,
data_contract_key="geointel.vector.geojson",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
source_registry=SimpleNamespace(
source_key=source_name,
classification="authoritative",
authority_scope_json={"scope": "coverage test"},
usage_policy_json={},
),
source_snapshot=SimpleNamespace(
source_registry_id=source_registry_id,
checksum_sha256=checksum_sha256,
freshness_status="current",
ingest_status="ingested",
),
)
def test_coverage_catalog_uses_normalized_contracts_and_does_not_change_provider_registry() -> None:
catalog = CoverageRegistryService.catalog()
@@ -135,9 +183,8 @@ def test_coverage_resolver_only_reports_operational_for_materialized_ready_datas
assert without_materialized.items[0].materialized_dataset_ids == []
dataset_id = uuid4()
materialized = SimpleNamespace(
id=dataset_id,
status="ready",
materialized = governed_materialization(
dataset_id=dataset_id,
source_name="ngi_adminvector",
reference_layer_name="belgium_regions",
source_metadata={"coverage_zones": ["belgium", "flanders", "wallonia", "brussels"]},
@@ -156,9 +203,8 @@ def test_coverage_resolver_only_reports_operational_for_materialized_ready_datas
def test_statbel_population_materialization_does_not_masquerade_as_admin_data() -> None:
project_id = uuid4()
statbel_id = uuid4()
statbel = SimpleNamespace(
id=statbel_id,
status="ready",
statbel = governed_materialization(
dataset_id=statbel_id,
source_name="statbel",
reference_layer_name="population",
source_metadata={"coverage_zones": ["belgium", "flanders", "wallonia", "brussels"]},
@@ -187,9 +233,8 @@ def test_statbel_population_materialization_does_not_masquerade_as_admin_data()
def test_bounded_api_materialization_only_covers_its_persisted_bbox() -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = SimpleNamespace(
id=dataset_id,
status="ready",
dataset = governed_materialization(
dataset_id=dataset_id,
source_name="spw_picc",
reference_layer_name="buildings",
source_metadata={
@@ -230,8 +275,24 @@ def test_bounded_partition_union_can_be_operational() -> None:
left_id = uuid4()
right_id = uuid4()
datasets = [
SimpleNamespace(id=left_id, status="ready", source_name="spw_picc", reference_layer_name="buildings", source_metadata={"coverage_zones": ["wallonia"], "bbox_epsg4326": [4.50, 50.50, 4.60, 50.60]}),
SimpleNamespace(id=right_id, status="ready", source_name="spw_picc", reference_layer_name="buildings", source_metadata={"coverage_zones": ["wallonia"], "bbox_epsg4326": [4.60, 50.50, 4.70, 50.60]}),
governed_materialization(
dataset_id=left_id,
source_name="spw_picc",
reference_layer_name="buildings",
source_metadata={
"coverage_zones": ["wallonia"],
"bbox_epsg4326": [4.50, 50.50, 4.60, 50.60],
},
),
governed_materialization(
dataset_id=right_id,
source_name="spw_picc",
reference_layer_name="buildings",
source_metadata={
"coverage_zones": ["wallonia"],
"bbox_epsg4326": [4.60, 50.50, 4.70, 50.60],
},
),
]
session = FakeSession(
project=SimpleNamespace(id=project_id),
@@ -252,9 +313,7 @@ def test_spw_bathymetry_materialization_is_source_specific() -> None:
scope_area("Wallonia", box(2.5, 49.5, 6.4, 50.8)),
]
selection = CoverageBBox(minx=4.851, miny=50.451, maxx=4.869, maxy=50.469)
spw_picc = SimpleNamespace(
id=uuid4(),
status="ready",
spw_picc = governed_materialization(
source_name="spw_picc",
reference_layer_name="buildings",
source_metadata={
@@ -272,9 +331,8 @@ def test_spw_bathymetry_materialization_is_source_specific() -> None:
assert without_bathymetry.items[0].materialized_dataset_ids == []
bathymetry_id = uuid4()
bathymetry = SimpleNamespace(
id=bathymetry_id,
status="ready",
bathymetry = governed_materialization(
dataset_id=bathymetry_id,
source_name="spw_bathymetry",
reference_layer_name=None,
source_metadata={
@@ -309,9 +367,8 @@ def test_vha_bathymetry_profiles_are_operational_only_inside_the_persisted_selec
assert without_profiles.items[0].source_names == ["vmm_vha_bathymetry_profiles"]
profile_id = uuid4()
profiles = SimpleNamespace(
id=profile_id,
status="ready",
profiles = governed_materialization(
dataset_id=profile_id,
source_name="vmm_vha_bathymetry_profiles",
reference_layer_name="bathymetry_profile_points",
source_metadata={
@@ -360,9 +417,8 @@ def test_flemish_materialization_is_theme_specific() -> None:
project_id = uuid4()
project = SimpleNamespace(id=project_id)
orthophoto_id = uuid4()
orthophoto = SimpleNamespace(
id=orthophoto_id,
status="ready",
orthophoto = governed_materialization(
dataset_id=orthophoto_id,
source_name="digitaal_vlaanderen_orthophoto",
reference_layer_name="orthophoto",
source_metadata={"coverage_zones": ["flanders"]},
@@ -429,5 +485,5 @@ def test_frontend_prefers_materialized_national_workspace_and_resolves_drawn_bbo
assert "externalApi.resolveCoverage" in coverage_hook
assert "coverage.outside_supported_scope" in map_workspace
assert "coverageStatusLabel" in map_workspace
assert "coverageSelectionAvailable" in map_workspace
assert "activeThemeSupportsCurrentSelection" in map_workspace
assert "activeThemeAvailable && !regionalPartitionedThemeActive" in map_workspace
+3 -1
View File
@@ -44,8 +44,10 @@ def _project_and_dataset():
project_id=project_id,
name="ortho.tif",
dataset_type="raster",
source="user_upload",
source="fixture",
source_name="fixture",
storage_path="storage/uploads/ortho.tif",
source_metadata={"fixture": True},
)
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
return db, project_id, dataset_id
@@ -0,0 +1,325 @@
from __future__ import annotations
from hashlib import sha256
import json
from pathlib import Path
from uuid import uuid4
import pytest
from app.core.errors import AppError
from app.models import DatasetQuarantine, SourceRegistry, SourceSnapshot
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
class FakeSession:
"""Explicit database double for production-runtime provenance tests."""
def __init__(self, objects: dict[tuple[type, object], object] | None = None) -> None:
self.objects = objects or {}
def get(self, model, item_id):
return self.objects.get((model, item_id))
def _write_sidecar(
model_path: Path,
*,
model_id: str = "yolo-configured",
task_type: str = "object_detection",
framework: str = "ultralytics/pytorch",
source_version: str = "test-v1",
source_registry_id: str | None = None,
source_snapshot_id: str | None = None,
) -> Path:
model_sha256 = sha256(model_path.read_bytes()).hexdigest()
payload = {
"schema_version": RuntimeModelProvenanceService.MANIFEST_SCHEMA_VERSION,
"data_contract": {
"key": "geointel.model.pytorch",
"version": "1.0.0",
},
"model": {
"model_id": model_id,
"task_type": task_type,
"sha256": model_sha256,
"model_format": "pytorch",
"framework": framework,
"class_mapping": {"0": "building"},
"source_version": source_version,
},
"source": {
"source_registry_id": source_registry_id or str(uuid4()),
"source_snapshot_id": source_snapshot_id or str(uuid4()),
"source_registry_key": "model",
"source_snapshot_checksum_sha256": model_sha256,
},
"lineage": {
"upstream_asset_ids": ["training-corpus:test-v1"],
"upstream_checksums_sha256": ["a" * 64],
"transformations": [
{
"name": "pytorch-training",
"version": "1.0.0",
"checksum_sha256": "b" * 64,
}
],
},
"metadata": {
"training_manifest_sha256": "c" * 64,
},
"imported_at": "2026-08-01T10:00:00+00:00",
}
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
sidecar_path = RuntimeModelProvenanceService.manifest_path_for_model(model_path)
sidecar_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
return sidecar_path
def _governed_model_database(
*,
source_registry_id,
source_snapshot_id,
model_checksum: str,
source_version: str = "test-v1",
) -> tuple[FakeSession, SourceRegistry, SourceSnapshot]:
registry = SourceRegistry(
id=source_registry_id,
source_key="model",
display_name="Governed test model artifacts",
classification="experimental",
authority_name="GeoIntel test fixture",
freshness_status="current",
ingest_status="configured",
)
snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key=f"model-{source_version}",
source_version=source_version,
checksum_sha256=model_checksum,
freshness_status="current",
ingest_status="ingested",
)
return (
FakeSession(
{
(SourceRegistry, source_registry_id): registry,
(SourceSnapshot, source_snapshot_id): snapshot,
}
),
registry,
snapshot,
)
def test_runtime_model_provenance_accepts_byte_bound_pytorch_sidecar_for_structural_preflight(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"trusted local model bytes")
sidecar_path = _write_sidecar(model_path, source_version="v1")
evidence = RuntimeModelProvenanceService.validate_for_runtime(
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
expected_model_version="v1",
allowed_frameworks=("ultralytics/pytorch",),
)
assert evidence.model_sha256 == sha256(model_path.read_bytes()).hexdigest()
assert evidence.manifest_path == str(sidecar_path.resolve())
assert evidence.data_contract_key == "geointel.model.pytorch"
assert evidence.data_contract_version == "1.0.0"
assert len(evidence.validation_report_sha256) == 64
def test_production_runtime_requires_db_bound_model_source_snapshot(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"governed local model bytes")
source_registry_id = uuid4()
source_snapshot_id = uuid4()
_write_sidecar(
model_path,
source_version="v1",
source_registry_id=str(source_registry_id),
source_snapshot_id=str(source_snapshot_id),
)
db, _, _ = _governed_model_database(
source_registry_id=source_registry_id,
source_snapshot_id=source_snapshot_id,
model_checksum=sha256(model_path.read_bytes()).hexdigest(),
source_version="v1",
)
evidence = RuntimeModelProvenanceService.validate_for_production_runtime(
db=db,
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
expected_model_version="v1",
allowed_frameworks=("ultralytics/pytorch",),
)
assert evidence.source_registry_id == str(source_registry_id)
assert evidence.source_snapshot_id == str(source_snapshot_id)
assert evidence.source_snapshot_checksum_sha256 == evidence.model_sha256
def test_production_runtime_rejects_missing_database_source_binding(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"unbound model bytes")
_write_sidecar(model_path)
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_production_runtime(
db=FakeSession(),
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_SOURCE_REGISTRY_NOT_FOUND"
def test_production_runtime_requires_a_database_session(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"model bytes")
_write_sidecar(model_path)
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_production_runtime(
db=None,
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_DATABASE_REQUIRED"
@pytest.mark.parametrize(
("mutation", "expected_code"),
(
("registry_unsafe", "MODEL_PROVENANCE_SOURCE_REGISTRY_UNSAFE"),
("snapshot_registry_mismatch", "MODEL_PROVENANCE_SOURCE_SNAPSHOT_REGISTRY_MISMATCH"),
("snapshot_missing", "MODEL_PROVENANCE_SOURCE_SNAPSHOT_NOT_FOUND"),
("snapshot_quarantined", "MODEL_PROVENANCE_SOURCE_SNAPSHOT_UNSAFE"),
("snapshot_checksum_mismatch", "MODEL_PROVENANCE_DATABASE_SNAPSHOT_CHECKSUM_MISMATCH"),
("active_quarantine", "MODEL_PROVENANCE_SOURCE_SNAPSHOT_QUARANTINED"),
),
)
def test_production_runtime_rejects_unsafe_or_inconsistent_database_snapshot(
tmp_path: Path,
mutation: str,
expected_code: str,
) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"governed model bytes")
source_registry_id = uuid4()
source_snapshot_id = uuid4()
_write_sidecar(
model_path,
source_registry_id=str(source_registry_id),
source_snapshot_id=str(source_snapshot_id),
)
db, registry, snapshot = _governed_model_database(
source_registry_id=source_registry_id,
source_snapshot_id=source_snapshot_id,
model_checksum=sha256(model_path.read_bytes()).hexdigest(),
)
if mutation == "registry_unsafe":
registry.ingest_status = "quarantined"
elif mutation == "snapshot_registry_mismatch":
snapshot.source_registry_id = uuid4()
elif mutation == "snapshot_missing":
db.objects.pop((SourceSnapshot, source_snapshot_id))
elif mutation == "snapshot_quarantined":
snapshot.ingest_status = "quarantined"
elif mutation == "snapshot_checksum_mismatch":
snapshot.checksum_sha256 = "f" * 64
elif mutation == "active_quarantine":
snapshot.quarantines = [
DatasetQuarantine(
source_snapshot_id=source_snapshot_id,
stage="test",
reason_code="test_active_quarantine",
status="quarantined",
)
]
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_production_runtime(
db=db,
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == expected_code
def test_runtime_model_provenance_rejects_missing_sidecar(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"unmanifested local model bytes")
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_runtime(
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_MANIFEST_MISSING"
def test_runtime_model_provenance_rejects_model_bytes_tampered_after_manifest(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"original local model bytes")
_write_sidecar(model_path)
model_path.write_bytes(b"tampered local model bytes")
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_runtime(
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_MODEL_CHECKSUM_MISMATCH"
def test_runtime_model_provenance_rejects_tampered_manifest_contents(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"original local model bytes")
sidecar_path = _write_sidecar(model_path)
payload = json.loads(sidecar_path.read_text(encoding="utf-8"))
payload["model"]["class_mapping"]["1"] = "road"
sidecar_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_runtime(
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_MANIFEST_CHECKSUM_MISMATCH"
def test_runtime_model_provenance_rejects_other_contract_even_if_structurally_valid(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local model bytes")
sidecar_path = _write_sidecar(model_path)
payload = json.loads(sidecar_path.read_text(encoding="utf-8"))
payload["data_contract"]["key"] = "geointel.vector.geojson"
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
sidecar_path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
with pytest.raises(AppError) as exc_info:
RuntimeModelProvenanceService.validate_for_runtime(
model_path=model_path,
model_id="yolo-configured",
task_type="object_detection",
)
assert exc_info.value.code == "MODEL_PROVENANCE_MANIFEST_INVALID"
@@ -1,5 +1,6 @@
from __future__ import annotations
from hashlib import sha256
import json
from pathlib import Path
from uuid import uuid4
@@ -8,9 +9,10 @@ import pytest
from geoalchemy2.shape import to_shape
from app.core.config import Settings
from app.models import Dataset, Project, Segmentation
from app.models import Dataset, Project, Segmentation, SourceRegistry, SourceSnapshot
from app.services.detection_georeferencing import pixel_points_to_epsg4326_polygon
from app.services.model_registry_service import ModelRegistryService
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
from app.services.segmentation_service import SegmentationService
ROOT = Path(__file__).resolve().parents[2]
@@ -80,18 +82,58 @@ class MissingDependencySegAdapter(AvailableSegAdapter):
return False
class NeverLoadSegAdapter(AvailableSegAdapter):
load_calls = 0
def load_model(self, model_path: Path):
type(self).load_calls += 1
raise AssertionError("unmanifested weights must not reach adapter.load_model")
def _project_and_dataset(dataset_type: str = "raster"):
project_id = uuid4()
dataset_id = uuid4()
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
project = Project(id=project_id, name="Mol")
source = SourceRegistry(
id=source_id,
source_key="digitaal_vlaanderen_orthophoto",
display_name="Governed orthophoto test source",
classification="contextual",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders", "role": "imagery"},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key="configured-segmentation-orthophoto",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset = Dataset(
id=dataset_id,
project_id=project_id,
name="ortho.tif",
dataset_type=dataset_type,
source="user_upload",
source="digitaal_vlaanderen_orthophoto",
source_name="digitaal_vlaanderen_orthophoto",
storage_path="storage/uploads/ortho.tif",
checksum_sha256=checksum,
source_registry_id=source_id,
source_snapshot_id=snapshot_id,
data_contract_key="geointel.raster.geotiff",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
status="ready",
)
dataset.source_registry = source
dataset.source_snapshot = snapshot
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
return db, project_id, dataset_id
@@ -108,6 +150,102 @@ def _settings(tmp_path: Path, **overrides) -> Settings:
return Settings(**values)
def _write_model_sidecar(
model_path: Path,
*,
model_id: str,
framework: str,
source_version: str | None,
db: FakeSession | None = None,
) -> None:
"""Create explicit local test evidence; no production code creates sidecars."""
model_sha256 = sha256(model_path.read_bytes()).hexdigest()
source_registry_id = uuid4()
source_snapshot_id = uuid4()
resolved_source_version = source_version or "test-v1"
if db is not None:
source_registry = SourceRegistry(
id=source_registry_id,
source_key="model",
display_name="Governed test model artifact",
classification="experimental",
authority_name="GeoIntel test fixture",
freshness_status="current",
ingest_status="configured",
)
source_snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key=f"model-{model_id}-{resolved_source_version}",
source_version=resolved_source_version,
checksum_sha256=model_sha256,
freshness_status="current",
ingest_status="ingested",
)
db.objects[(SourceRegistry, source_registry_id)] = source_registry
db.objects[(SourceSnapshot, source_snapshot_id)] = source_snapshot
payload = {
"schema_version": RuntimeModelProvenanceService.MANIFEST_SCHEMA_VERSION,
"data_contract": {"key": "geointel.model.pytorch", "version": "1.0.0"},
"model": {
"model_id": model_id,
"task_type": "segmentation",
"sha256": model_sha256,
"model_format": "pytorch",
"framework": framework,
"class_mapping": {"0": "segment"},
"source_version": resolved_source_version,
},
"source": {
"source_registry_id": str(source_registry_id),
"source_snapshot_id": str(source_snapshot_id),
"source_registry_key": "model",
"source_snapshot_checksum_sha256": model_sha256,
},
"lineage": {
"upstream_asset_ids": ["test-training-corpus"],
"upstream_checksums_sha256": ["a" * 64],
"transformations": [
{"name": "test-training", "version": "1.0.0", "checksum_sha256": "b" * 64}
],
},
"metadata": {"training_manifest_sha256": "c" * 64},
"imported_at": "2026-08-01T10:00:00+00:00",
}
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
RuntimeModelProvenanceService.manifest_path_for_model(model_path).write_text(
json.dumps(payload, sort_keys=True),
encoding="utf-8",
)
def _write_configured_model_sidecars(
tmp_path: Path,
settings: Settings,
*,
include_yolo: bool = True,
include_sam: bool = True,
db: FakeSession | None = None,
) -> None:
if include_yolo:
_write_model_sidecar(
tmp_path / "seg.pt",
model_id=settings.yolo_seg_model_id,
framework="ultralytics/pytorch",
source_version=settings.yolo_seg_model_version,
db=db,
)
if include_sam:
_write_model_sidecar(
tmp_path / "sam.pt",
model_id=settings.sam_model_id,
framework="ultralytics/sam",
source_version=settings.sam_model_version,
db=db,
)
def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
tiles = []
for index in range(tile_count):
@@ -172,10 +310,31 @@ def test_segmentation_models_report_dependency_unavailable(tmp_path: Path) -> No
assert models["sam-configured"].status == "dependency_unavailable"
def test_segmentation_models_require_runtime_provenance_sidecars(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"unmanifested yolo segmentation weights")
(tmp_path / "sam.pt").write_bytes(b"unmanifested sam weights")
settings = _settings(tmp_path)
models = {
model.model_id: model
for model in ModelRegistryService.list_segmentation_model_capabilities(
settings=settings,
yolo_seg_adapter_class=AvailableSegAdapter,
sam_adapter_class=ClassAgnosticSamAdapter,
)
}
assert models["yolo-seg-configured"].configured is False
assert models["yolo-seg-configured"].status == "contract_incomplete"
assert models["sam-configured"].configured is False
assert models["sam-configured"].status == "contract_incomplete"
def test_segmentation_models_report_configured_with_local_weights(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"weights")
(tmp_path / "sam.pt").write_bytes(b"weights")
settings = _settings(tmp_path)
_write_configured_model_sidecars(tmp_path, settings)
models = {
model.model_id: model
@@ -204,6 +363,7 @@ def test_configured_segmentation_requires_tile_manifest(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"weights")
db, project_id, dataset_id = _project_and_dataset()
settings = _settings(tmp_path)
_write_configured_model_sidecars(tmp_path, settings, include_sam=False, db=db)
with pytest.raises(Exception) as exc_info:
SegmentationService.run_segmentation(
@@ -220,10 +380,60 @@ def test_configured_segmentation_requires_tile_manifest(tmp_path: Path) -> None:
assert getattr(exc_info.value, "code", None) == "SEGMENTATION_TILE_MANIFEST_REQUIRED"
def test_configured_segmentation_fails_before_adapter_load_without_sidecar(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"unmanifested weights")
db, project_id, dataset_id = _project_and_dataset()
settings = _settings(tmp_path)
NeverLoadSegAdapter.load_calls = 0
response = SegmentationService.run_segmentation(
db=db,
project_id=project_id,
dataset_id=dataset_id,
model_id="yolo-seg-configured",
confidence_threshold=0.5,
tile_manifest_path=str(_manifest(tmp_path)),
settings=settings,
yolo_seg_adapter_class=NeverLoadSegAdapter,
sam_adapter_class=ClassAgnosticSamAdapter,
)
assert response.status == "failed"
assert response.error_code == "SEGMENTATION_MODEL_UNAVAILABLE"
assert NeverLoadSegAdapter.load_calls == 0
def test_configured_segmentation_rejects_unbound_model_snapshot_before_adapter_load(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"structurally valid but unbound weights")
db, project_id, dataset_id = _project_and_dataset()
settings = _settings(tmp_path)
# The sidecar passes catalog validation but its source registry/snapshot
# was never registered in this production-session fixture.
_write_configured_model_sidecars(tmp_path, settings, include_sam=False)
NeverLoadSegAdapter.load_calls = 0
response = SegmentationService.run_segmentation(
db=db,
project_id=project_id,
dataset_id=dataset_id,
model_id="yolo-seg-configured",
confidence_threshold=0.5,
tile_manifest_path=str(_manifest(tmp_path)),
settings=settings,
yolo_seg_adapter_class=NeverLoadSegAdapter,
sam_adapter_class=ClassAgnosticSamAdapter,
)
assert response.status == "failed"
assert response.error_code == "MODEL_PROVENANCE_SOURCE_REGISTRY_NOT_FOUND"
assert NeverLoadSegAdapter.load_calls == 0
def test_configured_yolo_seg_run_persists_georeferenced_masks(tmp_path: Path) -> None:
(tmp_path / "seg.pt").write_bytes(b"weights")
db, project_id, dataset_id = _project_and_dataset()
settings = _settings(tmp_path)
_write_configured_model_sidecars(tmp_path, settings, include_sam=False, db=db)
manifest_path = _manifest(tmp_path)
response = SegmentationService.run_segmentation(
@@ -255,12 +465,14 @@ def test_configured_yolo_seg_run_persists_georeferenced_masks(tmp_path: Path) ->
assert segmentation.area_m2 is not None and segmentation.area_m2 > 0
assert segmentation.provenance_json["inference"] == "local"
assert segmentation.provenance_json["model_id"] == "yolo-seg-configured"
assert segmentation.provenance_json["runtime_model_provenance"]["data_contract_key"] == "geointel.model.pytorch"
def test_configured_sam_run_is_class_agnostic(tmp_path: Path) -> None:
(tmp_path / "sam.pt").write_bytes(b"weights")
db, project_id, dataset_id = _project_and_dataset()
settings = _settings(tmp_path)
_write_configured_model_sidecars(tmp_path, settings, include_yolo=False, db=db)
manifest_path = _manifest(tmp_path)
response = SegmentationService.run_segmentation(
@@ -11,7 +11,7 @@ from shapely.geometry import box
from app.core.errors import AppError
from app.main import app
from app.models import Area, Dataset, Export
from app.models import Area, Dataset, Export, SourceRegistry, SourceSnapshot
from app.schemas.export import ExportCreateResponse
from app.services.export_service import ExportService
from app.services.storage_service import StorageService
@@ -45,18 +45,56 @@ class FakeSession:
return row
def _govern_fixture_dataset(dataset: Dataset) -> Dataset:
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB map export test source",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key=f"map-export-grb-{dataset.id}",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset.source = "grb"
dataset.source_name = "grb"
dataset.checksum_sha256 = checksum
dataset.source_registry_id = source_id
dataset.source_snapshot_id = snapshot_id
dataset.data_contract_key = "geointel.vector.geojson"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "not_applicable"
dataset.quarantine_status = "not_quarantined"
dataset.status = "ready"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_vector_selection_geojson_export_persists_handoff_artifact(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
export_path = tmp_path / "exports" / "selection.geojson"
dataset = Dataset(
dataset = _govern_fixture_dataset(Dataset(
id=dataset_id,
project_id=project_id,
name="candidate.geojson",
dataset_type="vector",
source="fixture",
status="ready",
)
))
db = FakeSession({(Dataset, dataset_id): dataset})
selection_bbox = {"min_x": 4.9, "min_y": 50.9, "max_x": 5.2, "max_y": 51.2, "crs": "EPSG:4326"}
selection_payload = {
@@ -135,14 +173,14 @@ def test_vector_selection_export_uses_exact_area_scope_when_requested(tmp_path,
project_id = uuid4()
dataset_id = uuid4()
area_id = uuid4()
dataset = Dataset(
dataset = _govern_fixture_dataset(Dataset(
id=dataset_id,
project_id=project_id,
name="regional-buildings.geojson",
dataset_type="vector",
source="fixture",
status="ready",
)
))
area_shape = box(5.0, 51.1, 5.2, 51.3)
area_geometry = from_shape(area_shape, srid=4326)
area = SimpleNamespace(id=area_id, project_id=project_id, name="Gemeente Mol", geometry=area_geometry)
@@ -184,7 +222,7 @@ def test_area_constrained_bbox_uses_intersection_and_disables_full_area_fast_pat
project_id = uuid4()
dataset_id = uuid4()
area_id = uuid4()
dataset = Dataset(
dataset = _govern_fixture_dataset(Dataset(
id=dataset_id,
project_id=project_id,
area_id=area_id,
@@ -193,7 +231,7 @@ def test_area_constrained_bbox_uses_intersection_and_disables_full_area_fast_pat
source="fixture",
source_metadata={"geometry_clipped_to_area": True},
status="ready",
)
))
area_shape = box(5.0, 51.0, 5.2, 51.2)
area = SimpleNamespace(
id=area_id,
@@ -7,7 +7,7 @@ from uuid import uuid4
from fastapi.testclient import TestClient
from app.main import app
from app.models import Dataset, VectorFeature
from app.models import Dataset
from app.schemas.dataset import DatasetCreateResponse
from app.services.storage_service import StorageService
from app.services.vector_feature_service import VectorFeatureService
@@ -124,7 +124,7 @@ def test_vector_selection_derive_persists_queryable_derived_dataset(tmp_path, mo
assert response.provenance_metadata["source_dataset_id"] == str(dataset_id)
assert response.provenance_metadata["source_table"] == "vector_features"
assert persisted_features[0]["dataset_id"] == derived.id
assert persisted_features[0]["commit"] is True
assert persisted_features[0]["commit"] is False
derived_payload = json.loads(output_path.read_text(encoding="utf-8"))
props = derived_payload["features"][0]["properties"]
assert props["source_vector_feature_id"] == "source-row-1"
@@ -75,5 +75,7 @@ def test_operator_yolo_train_smoke_script_contract() -> None:
assert '"dataset_summary_sha256"' in script
assert '"base_model_sha256"' in script
assert '"trained_model_sha256"' in script
assert "training_release_manifest.py" in script
assert "verify" in script
assert "download" not in script.lower()
assert "fixture_mode" not in script
+69 -4
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from hashlib import sha256
import json
import subprocess
import sys
@@ -9,6 +10,7 @@ from fastapi.testclient import TestClient
from app.core.config import Settings
from app.main import app
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
from app.services.yolo_preflight_service import YoloPreflightService
@@ -57,6 +59,43 @@ def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
return manifest_path
def _write_model_sidecar(model_path: Path, settings: Settings) -> None:
model_sha256 = sha256(model_path.read_bytes()).hexdigest()
payload = {
"schema_version": RuntimeModelProvenanceService.MANIFEST_SCHEMA_VERSION,
"data_contract": {"key": "geointel.model.pytorch", "version": "1.0.0"},
"model": {
"model_id": settings.yolo_model_id,
"task_type": "object_detection",
"sha256": model_sha256,
"model_format": "pytorch",
"framework": "ultralytics/pytorch",
"class_mapping": {"0": "building"},
"source_version": settings.yolo_model_version or "test-v1",
},
"source": {
"source_registry_id": "11111111-1111-4111-8111-111111111111",
"source_snapshot_id": "22222222-2222-4222-8222-222222222222",
"source_registry_key": "model",
"source_snapshot_checksum_sha256": model_sha256,
},
"lineage": {
"upstream_asset_ids": ["test-training-corpus"],
"upstream_checksums_sha256": ["a" * 64],
"transformations": [
{"name": "test-training", "version": "1.0.0", "checksum_sha256": "b" * 64}
],
},
"metadata": {"training_manifest_sha256": "c" * 64},
"imported_at": "2026-08-01T10:00:00+00:00",
}
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
RuntimeModelProvenanceService.manifest_path_for_model(model_path).write_text(
json.dumps(payload, sort_keys=True),
encoding="utf-8",
)
def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("YOLO_CONFIG_DIR", str(tmp_path / "ultralytics"))
@@ -99,9 +138,11 @@ def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path, tile_count=2)
settings = Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4)
_write_model_sidecar(model_path, settings)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
settings=settings,
tile_manifest_path=str(manifest_path),
yolo_adapter_class=AvailableAdapter,
)
@@ -109,6 +150,7 @@ def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_
assert result["status"] == "ready"
assert result["checks"]["dependencies_available"] is True
assert result["checks"]["model_file_exists"] is True
assert result["checks"]["model_provenance_valid"] is True
assert result["checks"]["manifest_valid"] is True
assert result["tile_count"] == 2
assert result["will_download_models"] is False
@@ -120,9 +162,11 @@ def test_yolo_preflight_marks_assumed_dependencies_in_runtime_details(tmp_path:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path, tile_count=1)
settings = Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4)
_write_model_sidecar(model_path, settings)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
settings=settings,
tile_manifest_path=str(manifest_path),
yolo_adapter_class=MissingDependencyAdapter,
assume_dependencies=True,
@@ -138,9 +182,11 @@ def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) ->
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path)
settings = Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4)
_write_model_sidecar(model_path, settings)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
settings=settings,
tile_manifest_path=str(manifest_path),
yolo_adapter_class=AvailableAdapter,
check_model_load=True,
@@ -156,9 +202,11 @@ def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) ->
def test_yolo_preflight_reports_explicit_model_load_failure(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
settings = Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4)
_write_model_sidecar(model_path, settings)
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
settings=settings,
tile_manifest_path=str(_manifest(tmp_path)),
yolo_adapter_class=FailingLoadAdapter,
check_model_load=True,
@@ -173,6 +221,7 @@ def test_yolo_preflight_script_outputs_json(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path)
_write_model_sidecar(model_path, Settings(yolo_enabled=True, yolo_model_path=str(model_path)))
result = subprocess.run(
[
@@ -201,6 +250,7 @@ def test_yolo_preflight_script_uses_environment_configuration(tmp_path: Path, mo
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"weights")
manifest_path = _manifest(tmp_path)
_write_model_sidecar(model_path, Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4))
monkeypatch.setenv("YOLO_ENABLED", "true")
monkeypatch.setenv("YOLO_MODEL_PATH", str(model_path))
monkeypatch.setenv("YOLO_MAX_TILES", "4")
@@ -227,6 +277,21 @@ def test_yolo_preflight_script_uses_environment_configuration(tmp_path: Path, mo
assert payload["max_tiles"] == 4
def test_yolo_preflight_refuses_unmanifested_local_weights(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"unmanifested weights")
result = YoloPreflightService.run(
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path)),
tile_manifest_path=str(_manifest(tmp_path)),
yolo_adapter_class=AvailableAdapter,
)
assert result["status"] == "contract_incomplete"
assert result["checks"]["model_provenance_valid"] is False
assert result["error_code"] == "MODEL_PROVENANCE_MANIFEST_MISSING"
def test_yolo_preflight_script_rejects_assumed_dependencies_for_model_load(tmp_path: Path) -> None:
result = subprocess.run(
[
@@ -8,7 +8,7 @@ from fastapi.testclient import TestClient
from app.core.errors import AppError
from app.main import app
from app.models import Area, Dataset, Export, Project, QualityCheck
from app.models import Area, Dataset, Export, Project, QualityCheck, SourceRegistry, SourceSnapshot
from app.schemas.export import ExportCreateResponse
from app.services.export_service import ExportService
from app.services.storage_service import StorageService
@@ -66,13 +66,57 @@ class FakeSession:
return row
def _govern_fixture_dataset(dataset: Dataset) -> Dataset:
"""Give an export fixture a governed authoritative source identity.
Export is a production boundary: test data must model a source that could
cross it, rather than using the deliberately QA-only ``fixture`` source.
"""
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB export test source",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key=f"export-grb-{dataset.id}",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset.source = "grb"
dataset.source_name = "grb"
dataset.checksum_sha256 = checksum
dataset.source_registry_id = source_id
dataset.source_snapshot_id = snapshot_id
dataset.data_contract_key = "geointel.vector.geojson"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "not_applicable"
dataset.quarantine_status = "not_quarantined"
dataset.status = "ready"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_dataset_geojson_export_persists_export_and_writes_artifact(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset_path = tmp_path / "input.geojson"
dataset_path.write_text(json.dumps({"type": "FeatureCollection", "features": []}), encoding="utf-8")
export_path = tmp_path / "exports" / "buildings.geojson"
dataset = Dataset(
dataset = _govern_fixture_dataset(Dataset(
id=dataset_id,
project_id=project_id,
name="buildings.geojson",
@@ -80,7 +124,7 @@ def test_dataset_geojson_export_persists_export_and_writes_artifact(tmp_path, mo
source="fixture",
storage_path=str(dataset_path),
status="ready",
)
))
db = FakeSession({(Dataset, dataset_id): dataset})
monkeypatch.setattr(StorageService, "dataset_export_path", lambda *_args: str(export_path))
@@ -18,7 +18,7 @@ from app.core.config import Settings
from app.core.errors import AppError
from app.db.session import get_db
from app.main import app
from app.models import Area, Dataset, DatasetVersion, Job, Project
from app.models import Area, Dataset, DatasetVersion, Job, Project, SourceRegistry, SourceSnapshot
from app.schemas.orthophoto import OrthophotoAcquireRequest
from app.services.orthophoto_acquisition_service import OrthophotoAcquisitionService
@@ -41,6 +41,15 @@ class FakeSession:
def add(self, row):
self.added.append(row)
def flush(self):
# The governed importer persists source identities and immutable
# snapshots before the Dataset. Mirror the database-generated UUIDs
# so this harness exercises that Phase 2 path rather than the legacy
# no-registry fallback.
for row in self.added:
if getattr(row, "id", None) is None:
row.id = uuid4()
def commit(self):
return None
@@ -50,13 +59,23 @@ class FakeSession:
def refresh(self, row):
return row
def query(self, _model):
return FakeQuery(self.query_result)
def query(self, model):
rows = [
row
for (row_model, _row_id), row in self.rows.items()
if row_model is model and isinstance(row, model)
]
rows.extend(row for row in self.added if isinstance(row, model))
if isinstance(self.query_result, model):
rows.append(self.query_result)
elif isinstance(self.query_result, list):
rows.extend(row for row in self.query_result if isinstance(row, model))
return FakeQuery(rows)
class FakeQuery:
def __init__(self, result):
self.result = result
def __init__(self, results):
self.results = list(results)
def filter(self, *_args):
return self
@@ -65,7 +84,10 @@ class FakeQuery:
return self
def first(self):
return self.result
return self.results[0] if self.results else None
def one_or_none(self):
return self.first()
class FakeImageResponse:
@@ -222,12 +244,23 @@ def test_regional_orthophoto_products_bind_provider_and_governed_scope(
)
dataset = next(row for row in db.added if isinstance(row, Dataset))
source = next(row for row in db.added if isinstance(row, SourceRegistry))
snapshot = next(row for row in db.added if isinstance(row, SourceSnapshot))
assert result["provider"] == provider
assert result["layer"] == layer
assert dataset.source_name == provider
assert dataset.source_metadata["coverage_zone"] == coverage_zone
assert dataset.source_metadata["license_note"]
assert dataset.provenance_metadata["request_url"].startswith(prepared["product"].wms_url)
assert dataset.source_registry_id == source.id
assert dataset.source_snapshot_id == snapshot.id
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.quarantine_status == "not_quarantined"
assert snapshot.source_registry_id == source.id
assert snapshot.checksum_sha256 == dataset.checksum_sha256
assert snapshot.ingest_status == "ingested"
assert snapshot.freshness_status == "current"
prepared = OrthophotoAcquisitionService._prepared_request(_selection_payload(product_key="1971"), settings)
assert prepared["params"]["LAYERS"] == "OKZPAN71VL"
@@ -291,6 +324,8 @@ def test_orthophoto_acquisition_persists_georeferenced_raster_and_provenance(tmp
assert len(datasets) == 1
assert len(versions) == 1
dataset = datasets[0]
source = next(row for row in db.added if isinstance(row, SourceRegistry))
snapshot = next(row for row in db.added if isinstance(row, SourceSnapshot))
assert result["output_dataset_id"] == str(dataset.id)
assert result["reused"] is False
assert dataset.project_id == project_id
@@ -301,6 +336,15 @@ def test_orthophoto_acquisition_persists_georeferenced_raster_and_provenance(tmp
assert dataset.crs == "EPSG:31370"
assert dataset.provenance_metadata["acquisition"] == "explicit_bounded_map_selection"
assert dataset.provenance_metadata["request_hash"] == prepared["request_hash"]
assert dataset.source_registry_id == source.id
assert dataset.source_snapshot_id == snapshot.id
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.lineage_status == "not_applicable"
assert dataset.quarantine_status == "not_quarantined"
assert snapshot.source_registry_id == source.id
assert snapshot.checksum_sha256 == dataset.checksum_sha256
assert snapshot.freshness_status == "current"
assert dataset.source_metadata["attribution"].startswith("Bron: Orthofotomozaiek Vlaanderen")
assert dataset.storage_path is not None
with rasterio.open(dataset.storage_path) as stored:
+40 -7
View File
@@ -17,7 +17,7 @@ from app.core.config import Settings
from app.core.errors import AppError
from app.db.session import get_db
from app.main import app
from app.models import Area, Dataset, DatasetVersion, Job, Project
from app.models import Area, Dataset, DatasetVersion, Job, Project, SourceRegistry, SourceSnapshot
from app.schemas.dhmv import DhmvAcquireRequest, TerrainPartitionSelectionRequest, TerrainSelectionRequest
from app.services.dhmv_acquisition_service import DhmvAcquisitionService
from app.services.terrain_analysis_service import TerrainAnalysisService
@@ -27,8 +27,8 @@ ROOT = Path(__file__).resolve().parents[2]
class FakeQuery:
def __init__(self, result=None):
self.result = result
def __init__(self, results=None):
self.results = list(results or [])
def filter(self, *_args):
return self
@@ -37,10 +37,13 @@ class FakeQuery:
return self
def first(self):
return self.result
return self.results[0] if self.results else None
def one_or_none(self):
return self.first()
def all(self):
return self.result if isinstance(self.result, list) else []
return list(self.results)
class FakeSession:
@@ -58,6 +61,14 @@ class FakeSession:
def add(self, row):
self.added.append(row)
def flush(self):
# Exercise the governed source/snapshot import path with database-like
# primary-key assignment instead of silently falling back to legacy
# fixture behavior.
for row in self.added:
if getattr(row, "id", None) is None:
row.id = uuid4()
def commit(self):
return None
@@ -67,8 +78,18 @@ class FakeSession:
def refresh(self, row):
return row
def query(self, _model):
return FakeQuery(self.query_result)
def query(self, model):
rows = [
row
for (row_model, _row_id), row in self.rows.items()
if row_model is model and isinstance(row, model)
]
rows.extend(row for row in self.added if isinstance(row, model))
if isinstance(self.query_result, model):
rows.append(self.query_result)
elif isinstance(self.query_result, list):
rows.extend(row for row in self.query_result if isinstance(row, model))
return FakeQuery(rows)
class FakeResponse:
@@ -318,6 +339,8 @@ def test_dhmv_acquisition_clips_validates_and_persists_via_dataset_service(tmp_p
dataset = next(item for item in db.added if isinstance(item, Dataset))
version = next(item for item in db.added if isinstance(item, DatasetVersion))
source = next(item for item in db.added if isinstance(item, SourceRegistry))
snapshot = next(item for item in db.added if isinstance(item, SourceSnapshot))
assert result["output_dataset_id"] == str(dataset.id)
assert dataset.source_name == "digitaal_vlaanderen_dhmv"
assert dataset.area_id == area_id
@@ -331,6 +354,16 @@ def test_dhmv_acquisition_clips_validates_and_persists_via_dataset_service(tmp_p
assert dataset.provenance_metadata["water_depth_available"] is False
assert dataset.provenance_metadata["water_volume_available"] is False
assert len(dataset.provenance_metadata["response_sha256"]) == 64
assert dataset.source_registry_id == source.id
assert dataset.source_snapshot_id == snapshot.id
assert dataset.validation_status == "passed"
assert dataset.provenance_status == "complete"
assert dataset.lineage_status == "not_applicable"
assert dataset.quarantine_status == "not_quarantined"
assert snapshot.source_registry_id == source.id
assert snapshot.checksum_sha256 == dataset.checksum_sha256
assert snapshot.ingest_status == "ingested"
assert snapshot.freshness_status == "current"
with rasterio.open(dataset.storage_path) as stored:
assert stored.crs.to_epsg() == 31370
assert stored.count == 1
@@ -1,8 +1,9 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from hashlib import sha256
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import pytest
@@ -10,12 +11,13 @@ from fastapi.testclient import TestClient
from pydantic import ValidationError
from app.main import app
from app.models import Dataset, Export
from app.models import Dataset, Export, SourceRegistry, SourceSnapshot
from app.schemas.export import ExportCreateResponse, MapResultExportRequest
from app.schemas.project import ProjectRead
from app.services.export_service import ExportService
from app.services.project_service import ProjectService
from app.services.storage_service import StorageService
from app.services.source_registry_service import SourceRegistryService
from app.services.temporal_analysis_service import TemporalAnalysisService
from app.services.thematic_raster_analysis_service import ThematicRasterAnalysisService
@@ -48,6 +50,71 @@ def bbox_payload() -> dict:
}
def governed_dataset(
*,
project_id,
dataset_id,
name: str,
dataset_type: str,
source_key: str,
dataset_role: str = "source",
) -> Dataset:
"""Build an in-memory stand-in for a passed governed dataset.
Map-result export is an operational consumption boundary. These tests
must therefore model the same source registry/snapshot, checksum and
passed-contract evidence supplied by a real adapter rather than relying
on an old transient Dataset fixture.
"""
source = SourceRegistry(
id=uuid4(),
**SourceRegistryService.definition_for(source_key).as_model_values(),
)
checksum = sha256(f"{dataset_id}:{source_key}:{dataset_type}".encode("utf-8")).hexdigest()
snapshot = SourceSnapshot(
id=uuid4(),
source_registry_id=source.id,
snapshot_key=f"test:{source_key}:{checksum}",
checksum_sha256=checksum,
fetched_at=datetime.now(UTC),
crs="EPSG:31370",
units=source.default_units,
spatial_resolution_json={"x": 1.0, "y": 1.0, "unit": "m"},
temporal_coverage_json={"status": "test-fixture"},
geographic_coverage_json={"zone": "Flanders"},
observed_schema_json={"dataset_type": dataset_type},
freshness_status="current",
ingest_status="ingested",
known_limitations_json=["In-memory governed fixture used only by this export test."],
snapshot_metadata_json={"fixture_mode": True},
)
return Dataset(
id=dataset_id,
project_id=project_id,
name=name,
dataset_type=dataset_type,
source="governed test fixture",
dataset_role=dataset_role,
source_name=source.source_key,
source_registry_id=source.id,
source_snapshot_id=snapshot.id,
source_registry=source,
source_snapshot=snapshot,
data_contract_key=("geointel.raster.geotiff" if dataset_type == "raster" else "geointel.vector.geojson"),
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
checksum_sha256=checksum,
metadata_json={"fixture_mode": True},
source_metadata={"fixture_mode": True, "source_registry_key": source.source_key},
provenance_metadata={"fixture_mode": True, "source_snapshot_id": str(snapshot.id)},
status="ready",
)
def test_map_result_export_request_requires_a_complete_target() -> None:
with pytest.raises(ValidationError):
MapResultExportRequest(project_id=uuid4(), mode="current", bbox=bbox_payload())
@@ -67,13 +134,12 @@ def test_current_vector_map_result_uses_authoritative_selection_export(monkeypat
project_id = uuid4()
dataset_id = uuid4()
area_id = uuid4()
dataset = Dataset(
id=dataset_id,
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="buildings.geojson",
dataset_type="vector",
source="fixture",
status="ready",
source_key="grb",
)
db = FakeSession({(Dataset, dataset_id): dataset})
expected = ExportCreateResponse(
@@ -109,14 +175,12 @@ def test_current_vector_map_result_uses_authoritative_selection_export(monkeypat
def test_partitioned_vector_map_result_uses_governed_partition_export(monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = Dataset(
id=dataset_id,
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="vha-municipality.geojson",
dataset_type="vector",
source="VHA",
source_name="vmm_vha_bathymetry_profiles",
status="ready",
source_key="vmm_vha_bathymetry_profiles",
)
db = FakeSession({(Dataset, dataset_id): dataset})
expected = ExportCreateResponse(
@@ -159,14 +223,12 @@ def test_partitioned_vector_map_result_uses_governed_partition_export(monkeypatc
def test_raster_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch) -> None:
project_id = uuid4()
dataset_id = uuid4()
dataset = Dataset(
id=dataset_id,
dataset = governed_dataset(
project_id=project_id,
dataset_id=dataset_id,
name="space-occupation.tif",
dataset_type="raster",
source="official",
source_name="department_omgeving_thematic_raster",
status="ready",
source_key="department_omgeving_thematic_raster",
)
db = FakeSession({(Dataset, dataset_id): dataset})
export_path = tmp_path / "space-occupation-analysis.json"
@@ -209,7 +271,26 @@ def test_evolution_map_result_is_recomputed_and_persisted(tmp_path, monkeypatch)
project_id = uuid4()
earlier_id = uuid4()
later_id = uuid4()
db = FakeSession()
earlier_dataset = governed_dataset(
project_id=project_id,
dataset_id=earlier_id,
name="forest-earlier.geojson",
dataset_type="vector",
source_key="inbo_bwk_natura2000",
)
later_dataset = governed_dataset(
project_id=project_id,
dataset_id=later_id,
name="forest-later.geojson",
dataset_type="vector",
source_key="inbo_bwk_natura2000",
)
db = FakeSession(
{
(Dataset, earlier_id): earlier_dataset,
(Dataset, later_id): later_dataset,
}
)
export_path = tmp_path / "forest-evolution.json"
captured: dict = {}
@@ -6,6 +6,7 @@ from uuid import uuid4
import pytest
from geoalchemy2.shape import to_shape
from pyproj import Transformer
from app.api.routes.qa import compare_candidate_with_reference
from app.models import Dataset, Metric, Project, QualityCheck, VectorFeature
@@ -117,6 +118,54 @@ def test_vector_feature_service_normalizes_source_z_coordinates_to_canonical_2d(
assert to_shape(persisted[0].geometry).has_z is False
def test_vector_feature_service_transforms_declared_source_crs_before_epsg4326_storage() -> None:
to_lambert = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
x, y = to_lambert.transform(4.7, 51.1)
db = FakeSession()
persisted = VectorFeatureService.persist_geojson_features(
db=db,
dataset_id=uuid4(),
payload={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "lambert-point",
"properties": {},
"geometry": {"type": "Point", "coordinates": [x, y]},
}
],
},
source_crs="EPSG:31370",
)
geometry = to_shape(persisted[0].geometry)
assert geometry.x == pytest.approx(4.7, abs=0.000001)
assert geometry.y == pytest.approx(51.1, abs=0.000001)
def test_vector_feature_service_rejects_invalid_declared_source_crs() -> None:
with pytest.raises(Exception) as exc_info:
VectorFeatureService.persist_geojson_features(
db=FakeSession(),
dataset_id=uuid4(),
payload={
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {"type": "Point", "coordinates": [4.7, 51.1]},
}
],
},
source_crs="EPSG:not-a-crs",
)
assert getattr(exc_info.value, "code", None) == "INVALID_DATASET_CRS"
def test_dataset_upload_persists_vector_features(monkeypatch, tmp_path) -> None:
project_id = uuid4()
db = FakeSession(objects={(Project, project_id): Project(id=project_id, name="Geel")})
@@ -43,7 +43,7 @@ def _project_and_dataset(dataset_type: str = "raster"):
project_id=project_id,
name="source.tif",
dataset_type=dataset_type,
source="user_upload",
source="test",
storage_path="storage/uploads/source.tif",
)
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
+191 -3
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from hashlib import sha256
import json
from pathlib import Path
import sys
@@ -10,10 +11,11 @@ import pytest
from app.core.config import Settings
from app.core.errors import AppError
from app.models import AnalysisRun, Area, Dataset, Detection, Job, Project
from app.models import AnalysisRun, Area, Dataset, Detection, Job, Project, SourceRegistry, SourceSnapshot
from app.services.detection_georeferencing import pixel_bbox_to_epsg4326_polygon
from app.services.detection_service import DetectionService
from app.services.model_registry_service import ModelRegistryService
from app.services.runtime_model_provenance_service import RuntimeModelProvenanceService
from app.services.yolo_adapter import YoloDetectionAdapter
ROOT = Path(__file__).resolve().parents[2]
@@ -79,6 +81,14 @@ class MockYoloAdapter:
]
class NeverLoadUnboundModelAdapter(MockYoloAdapter):
load_calls = 0
def load_model(self, model_path: Path):
type(self).load_calls += 1
raise AssertionError("unbound model provenance must be rejected before adapter.load_model")
class MixedCaseYoloAdapter(MockYoloAdapter):
def predict_tile(self, model, tile_path: Path, confidence_threshold: float) -> list[dict]:
return [
@@ -141,15 +151,47 @@ class ExplodingPredictModel:
def _project_and_dataset(dataset_type: str = "raster"):
project_id = uuid4()
dataset_id = uuid4()
source_registry_id = uuid4()
source_snapshot_id = uuid4()
checksum = "a" * 64
project = Project(id=project_id, name="Geel")
source_registry = SourceRegistry(
id=source_registry_id,
source_key="test-derived-raster",
display_name="Governed test-derived raster",
classification="derived",
authority_name="GeoIntel test fixture",
usage_policy_json={"ground_truth_allowed": False},
)
source_snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key="test-derived-raster-v1",
checksum_sha256=checksum,
freshness_status="current",
ingest_status="ingested",
)
dataset = Dataset(
id=dataset_id,
project_id=project_id,
name="source.tif",
dataset_type=dataset_type,
source="user_upload",
source="test-derived-raster",
source_name="test-derived-raster",
storage_path="storage/uploads/source.tif",
checksum_sha256=checksum,
source_registry_id=source_registry_id,
source_snapshot_id=source_snapshot_id,
data_contract_key="geointel.raster.geotiff",
data_contract_version="1.0.0",
validation_status="passed",
provenance_status="complete",
lineage_status="not_applicable",
quarantine_status="not_quarantined",
status="ready",
)
dataset.source_registry = source_registry
dataset.source_snapshot = source_snapshot
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
return db, project_id, dataset_id
@@ -165,6 +207,74 @@ def _settings(tmp_path: Path, **overrides) -> Settings:
return Settings(**values)
def _write_model_sidecar(
model_path: Path,
settings: Settings,
*,
db: FakeSession | None = None,
) -> None:
"""Create explicit test-only evidence; production never self-generates it."""
model_sha256 = sha256(model_path.read_bytes()).hexdigest()
source_registry_id = uuid4()
source_snapshot_id = uuid4()
source_version = settings.yolo_model_version or "test-v1"
if db is not None:
source_registry = SourceRegistry(
id=source_registry_id,
source_key="model",
display_name="Governed test model artifact",
classification="experimental",
authority_name="GeoIntel test fixture",
freshness_status="current",
ingest_status="configured",
)
source_snapshot = SourceSnapshot(
id=source_snapshot_id,
source_registry_id=source_registry_id,
snapshot_key=f"model-{source_version}",
source_version=source_version,
checksum_sha256=model_sha256,
freshness_status="current",
ingest_status="ingested",
)
db.objects[(SourceRegistry, source_registry_id)] = source_registry
db.objects[(SourceSnapshot, source_snapshot_id)] = source_snapshot
payload = {
"schema_version": RuntimeModelProvenanceService.MANIFEST_SCHEMA_VERSION,
"data_contract": {"key": "geointel.model.pytorch", "version": "1.0.0"},
"model": {
"model_id": settings.yolo_model_id,
"task_type": "object_detection",
"sha256": model_sha256,
"model_format": "pytorch",
"framework": "ultralytics/pytorch",
"class_mapping": {"0": "building"},
"source_version": source_version,
},
"source": {
"source_registry_id": str(source_registry_id),
"source_snapshot_id": str(source_snapshot_id),
"source_registry_key": "model",
"source_snapshot_checksum_sha256": model_sha256,
},
"lineage": {
"upstream_asset_ids": ["test-training-corpus"],
"upstream_checksums_sha256": ["a" * 64],
"transformations": [
{"name": "test-training", "version": "1.0.0", "checksum_sha256": "b" * 64}
],
},
"metadata": {"training_manifest_sha256": "c" * 64},
"imported_at": "2026-08-01T10:00:00+00:00",
}
payload["metadata"]["runtime_manifest_sha256"] = RuntimeModelProvenanceService.manifest_self_checksum(payload)
RuntimeModelProvenanceService.manifest_path_for_model(model_path).write_text(
json.dumps(payload, sort_keys=True),
encoding="utf-8",
)
def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
tiles = []
for index in range(tile_count):
@@ -223,10 +333,28 @@ def test_yolo_configured_model_reports_dependency_unavailable(tmp_path: Path) ->
assert model.status == "dependency_unavailable"
def test_yolo_configured_model_requires_a_runtime_provenance_sidecar(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"unmanifested local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
model = ModelRegistryService.get_model_capability(
"yolo-configured",
settings=settings,
yolo_adapter_class=AvailableAdapter,
)
assert model is not None
assert model.configured is False
assert model.status == "contract_incomplete"
assert "sidecar" in model.limitation_message
def test_yolo_configured_model_reports_configured_with_local_model_and_dependencies(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
_write_model_sidecar(model_path, settings)
model = ModelRegistryService.get_model_capability("yolo-configured", settings=settings, yolo_adapter_class=AvailableAdapter)
@@ -306,11 +434,37 @@ def test_yolo_run_requires_tile_manifest_path(tmp_path: Path) -> None:
assert getattr(exc_info.value, "code", None) == "DETECTION_TILE_MANIFEST_REQUIRED"
def test_yolo_run_fails_closed_before_adapter_load_without_sidecar(tmp_path: Path) -> None:
db, project_id, dataset_id = _project_and_dataset()
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"unmanifested local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
# AvailableAdapter intentionally has no load_model method. If runtime
# provenance were checked after adapter loading, this would raise instead
# of returning the explicit unavailable capability state.
result = DetectionService.run_detection(
db=db,
project_id=project_id,
dataset_id=dataset_id,
model_id="yolo-configured",
confidence_threshold=0.5,
tile_manifest_path=str(_manifest(tmp_path)),
settings=settings,
yolo_adapter_class=AvailableAdapter,
)
assert result.status == "failed"
assert result.error_code == "DETECTION_MODEL_UNAVAILABLE"
assert "sidecar" in result.message
def test_yolo_run_rejects_manifest_over_tile_limit(tmp_path: Path) -> None:
db, project_id, dataset_id = _project_and_dataset()
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path), yolo_max_tiles=1)
_write_model_sidecar(model_path, settings, db=db)
manifest_path = _manifest(tmp_path, tile_count=2)
result = DetectionService.run_detection(
@@ -333,6 +487,7 @@ def test_yolo_run_rejects_missing_tile_manifest_file(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
_write_model_sidecar(model_path, settings, db=db)
result = DetectionService.run_detection(
db=db,
@@ -354,6 +509,7 @@ def test_yolo_run_rejects_invalid_tile_manifest_json(tmp_path: Path) -> None:
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
_write_model_sidecar(model_path, settings, db=db)
manifest_path = tmp_path / "manifest.json"
manifest_path.write_text("{not-json", encoding="utf-8")
@@ -372,6 +528,32 @@ def test_yolo_run_rejects_invalid_tile_manifest_json(tmp_path: Path) -> None:
assert result.error_code == "DETECTION_TILE_MANIFEST_INVALID"
def test_yolo_run_rejects_unbound_model_snapshot_before_adapter_load(tmp_path: Path) -> None:
db, project_id, dataset_id = _project_and_dataset()
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"structurally valid but unbound model")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
# A catalog/preflight sidecar alone is deliberately insufficient for a
# production call. Do not register the declared source IDs in ``db``.
_write_model_sidecar(model_path, settings)
NeverLoadUnboundModelAdapter.load_calls = 0
result = DetectionService.run_detection(
db=db,
project_id=project_id,
dataset_id=dataset_id,
model_id="yolo-configured",
confidence_threshold=0.5,
tile_manifest_path=str(_manifest(tmp_path)),
settings=settings,
yolo_adapter_class=NeverLoadUnboundModelAdapter,
)
assert result.status == "failed"
assert result.error_code == "MODEL_PROVENANCE_SOURCE_REGISTRY_NOT_FOUND"
assert NeverLoadUnboundModelAdapter.load_calls == 0
def test_pixel_bbox_to_epsg4326_polygon_from_gdal_transform() -> None:
polygon = pixel_bbox_to_epsg4326_polygon(
bbox=[10, 20, 30, 40],
@@ -390,6 +572,7 @@ def test_yolo_run_persists_mocked_georeferenced_detections(tmp_path: Path) -> No
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path), yolo_model_version="local-test")
_write_model_sidecar(model_path, settings, db=db)
manifest_path = _manifest(tmp_path, tile_count=1)
result = DetectionService.run_detection(
@@ -416,7 +599,10 @@ def test_yolo_run_persists_mocked_georeferenced_detections(tmp_path: Path) -> No
assert detections[0].confidence == 0.91
assert detections[0].source_tile_path.endswith("tile_0000.tif")
assert detections[0].bbox_json == {"x_min": 10.0, "y_min": 20.0, "x_max": 30.0, "y_max": 40.0}
assert detections[0].properties_json == {"adapter": "mock", "tile_index": 0}
assert detections[0].properties_json["adapter"] == "mock"
assert detections[0].properties_json["tile_index"] == 0
assert detections[0].properties_json["runtime_model_provenance"]["model_sha256"] == sha256(model_path.read_bytes()).hexdigest()
assert runs[0].parameters_json["runtime_model_provenance"]["data_contract_key"] == "geointel.model.pytorch"
assert runs[0].status == "success"
assert jobs[0].status == "success"
@@ -426,6 +612,7 @@ def test_yolo_class_filter_is_case_insensitive_and_persists_canonical_class(tmp_
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path))
_write_model_sidecar(model_path, settings, db=db)
manifest_path = _manifest(tmp_path, tile_count=1)
result = DetectionService.run_detection(
@@ -453,6 +640,7 @@ def test_yolo_run_suppresses_cross_tile_duplicate_detections(tmp_path: Path) ->
model_path = tmp_path / "model.pt"
model_path.write_bytes(b"local weights")
settings = _settings(tmp_path, yolo_model_path=str(model_path), yolo_duplicate_iou_threshold=0.5)
_write_model_sidecar(model_path, settings, db=db)
manifest_path = _manifest(tmp_path, tile_count=2)
result = DetectionService.run_detection(
@@ -12,7 +12,7 @@ from shapely.geometry import Polygon, box
from app.core.errors import AppError
from app.main import app
from app.db.session import get_db
from app.models import AnalysisRun, Dataset, Detection, Metric, QualityCheck, VectorFeature
from app.models import AnalysisRun, Dataset, Detection, Metric, QualityCheck, SourceRegistry, SourceSnapshot, VectorFeature
from app.services.detection_service import DetectionService
@@ -96,11 +96,52 @@ def _source_dataset(project_id, dataset_id):
project_id=project_id,
name="source.tif",
dataset_type="raster",
source="manual",
source_name="manual",
source="test",
source_name="test",
)
def _authoritative_reference(dataset: Dataset) -> Dataset:
"""Model the reference as a fully governed GRB fixture, never test data."""
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB QA fixture",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True, "validation_authority": {"building_validation": "primary"}},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key=f"detection-qa-{dataset.id}",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset.source = "grb"
dataset.source_name = "grb"
dataset.dataset_role = "reference"
dataset.status = "ready"
dataset.checksum_sha256 = checksum
dataset.source_registry_id = source_id
dataset.source_snapshot_id = snapshot_id
dataset.data_contract_key = "geointel.vector.geojson"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "complete"
dataset.quarantine_status = "not_quarantined"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_detection_geojson_feature_collection_shape() -> None:
project_id = uuid4()
dataset_id = uuid4()
@@ -201,14 +242,14 @@ def test_detection_qa_persists_quality_check_and_metrics() -> None:
reference_dataset_id = uuid4()
analysis_run_id = uuid4()
detection = _detection(project_id, dataset_id, analysis_run_id, geom=box(0, 0, 1, 1))
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -262,7 +303,7 @@ def test_detection_qa_rejects_non_overlapping_historical_reference_editions() ->
source_dataset.source_metadata = {"product_key": "2020", "supports_detection": False}
source_dataset.valid_from = datetime(2020, 1, 1, tzinfo=UTC)
source_dataset.valid_to = datetime(2020, 12, 31, 23, 59, 59, tzinfo=UTC)
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="current-grb.geojson",
@@ -272,7 +313,7 @@ def test_detection_qa_rejects_non_overlapping_historical_reference_editions() ->
dataset_role="reference",
valid_from=datetime(2026, 7, 1, tzinfo=UTC),
valid_to=datetime(2026, 7, 31, 23, 59, 59, tzinfo=UTC),
)
))
db = FakeSession(
objects={
(AnalysisRun, analysis_run_id): AnalysisRun(
@@ -305,14 +346,14 @@ def test_detection_qa_no_match_case_persists_zero_scores() -> None:
reference_dataset_id = uuid4()
analysis_run_id = uuid4()
detection = _detection(project_id, dataset_id, analysis_run_id, geom=box(0, 0, 1, 1))
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -349,14 +390,14 @@ def test_configured_yolo_qa_requires_persisted_tile_manifest_provenance() -> Non
reference_dataset_id = uuid4()
analysis_run_id = uuid4()
detection = _detection(project_id, dataset_id, analysis_run_id)
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -421,14 +462,14 @@ def test_detection_qa_excludes_references_outside_persisted_tile_coverage(tmp_pa
analysis_run_id = uuid4()
manifest_path = _coverage_manifest(tmp_path, dataset_id, bounds=(0.0, 0.0, 1.0, 1.0))
detection = _detection(project_id, dataset_id, analysis_run_id, geom=box(0.1, 0.1, 0.9, 0.9))
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
inside_reference = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -489,14 +530,14 @@ def test_detection_qa_reports_box_to_footprint_diagnostic_without_changing_stric
l_shaped_footprint = Polygon(
[(0.0, 0.0), (2.0, 0.0), (2.0, 0.4), (0.4, 0.4), (0.4, 2.0), (0.0, 2.0), (0.0, 0.0)]
)
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -10,7 +10,18 @@ from shapely.geometry import MultiPolygon, box, mapping
from app.db.session import get_db
from app.main import app
from app.models import AnalysisRun, Dataset, Job, Metric, Project, QualityCheck, Segmentation, VectorFeature
from app.models import (
AnalysisRun,
Dataset,
Job,
Metric,
Project,
QualityCheck,
Segmentation,
SourceRegistry,
SourceSnapshot,
VectorFeature,
)
from app.services.model_registry_service import ModelRegistryService
from app.services.segmentation_service import SegmentationService
@@ -75,7 +86,7 @@ def _project_and_dataset(dataset_type: str = "raster"):
project_id=project_id,
name="source.tif",
dataset_type=dataset_type,
source="user_upload",
source="test",
storage_path="storage/uploads/source.tif",
)
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
@@ -105,6 +116,47 @@ def _segmentation(project_id, dataset_id, analysis_run_id, class_name="vegetatio
)
def _authoritative_reference(dataset: Dataset) -> Dataset:
"""Model segmentation QA references as governed authoritative GRB evidence."""
source_id = uuid4()
snapshot_id = uuid4()
checksum = "a" * 64
source = SourceRegistry(
id=source_id,
source_key="grb",
display_name="GRB segmentation QA fixture",
classification="authoritative",
authority_name="Digitaal Vlaanderen",
authority_scope_json={"zone": "Flanders"},
usage_policy_json={"ground_truth_allowed": True, "validation_authority": {"building_validation": "primary"}},
)
snapshot = SourceSnapshot(
id=snapshot_id,
source_registry_id=source_id,
snapshot_key=f"segmentation-qa-{dataset.id}",
checksum_sha256=checksum,
ingest_status="ingested",
freshness_status="current",
)
dataset.source = "grb"
dataset.source_name = "grb"
dataset.dataset_role = "reference"
dataset.status = "ready"
dataset.checksum_sha256 = checksum
dataset.source_registry_id = source_id
dataset.source_snapshot_id = snapshot_id
dataset.data_contract_key = "geointel.vector.geojson"
dataset.data_contract_version = "1.0.0"
dataset.validation_status = "passed"
dataset.provenance_status = "complete"
dataset.lineage_status = "complete"
dataset.quarantine_status = "not_quarantined"
dataset.source_registry = source
dataset.source_snapshot = snapshot
return dataset
def test_model_registry_returns_segmentation_states() -> None:
models = {model.model_id: model for model in ModelRegistryService.list_model_capabilities(task_type="segmentation")}
@@ -265,14 +317,14 @@ def test_segmentation_qa_persists_quality_check_and_metrics() -> None:
reference_dataset_id = uuid4()
analysis_run_id = uuid4()
segmentation = _segmentation(project_id, dataset_id, analysis_run_id, geom=box(0, 0, 1, 1))
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -282,6 +334,7 @@ def test_segmentation_qa_persists_quality_check_and_metrics() -> None:
db = FakeSession(
objects={
(AnalysisRun, analysis_run_id): AnalysisRun(id=analysis_run_id, project_id=project_id, dataset_id=dataset_id, analysis_type="segmentation", status="success", parameters_json={}),
(Dataset, dataset_id): Dataset(id=dataset_id, project_id=project_id, name="fixture.tif", dataset_type="raster", source="test"),
(Dataset, reference_dataset_id): reference_dataset,
},
query_rows={Segmentation: [segmentation], VectorFeature: [reference_feature]},
@@ -318,14 +371,14 @@ def test_segmentation_qa_no_match_case_persists_zero_scores() -> None:
reference_dataset_id = uuid4()
analysis_run_id = uuid4()
segmentation = _segmentation(project_id, dataset_id, analysis_run_id, geom=box(0, 0, 1, 1))
reference_dataset = Dataset(
reference_dataset = _authoritative_reference(Dataset(
id=reference_dataset_id,
project_id=project_id,
name="reference.geojson",
dataset_type="vector",
source="manual",
source="test",
dataset_role="reference",
)
))
reference_feature = VectorFeature(
id=uuid4(),
dataset_id=reference_dataset_id,
@@ -335,6 +388,7 @@ def test_segmentation_qa_no_match_case_persists_zero_scores() -> None:
db = FakeSession(
objects={
(AnalysisRun, analysis_run_id): AnalysisRun(id=analysis_run_id, project_id=project_id, dataset_id=dataset_id, analysis_type="segmentation", status="success", parameters_json={}),
(Dataset, dataset_id): Dataset(id=dataset_id, project_id=project_id, name="fixture.tif", dataset_type="raster", source="test"),
(Dataset, reference_dataset_id): reference_dataset,
},
query_rows={Segmentation: [segmentation], VectorFeature: [reference_feature]},
@@ -0,0 +1,410 @@
from __future__ import annotations
import importlib.util
import hashlib
import json
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "training_dataset_eligibility.py"
SPEC = importlib.util.spec_from_file_location("training_dataset_eligibility", SCRIPT)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
CHECKSUM = "a" * 64
UNSET = object()
def source_registry(
*,
classification: str = "authoritative",
training_allowed: bool = True,
ground_truth_allowed: bool = True,
allowed_tasks: list[str] | None = None,
building_validation_authority: str = "primary",
) -> SimpleNamespace:
return SimpleNamespace(
id="source-registry-1",
source_key="governed-source",
classification=classification,
freshness_status="current",
ingest_status="ingested",
usage_policy_json={
"training_allowed": training_allowed,
"ground_truth_allowed": ground_truth_allowed,
"allowed_tasks": allowed_tasks or ["building_validation", "building_labels"],
"validation_authority": {
"building_validation": building_validation_authority,
},
},
)
def source_snapshot(*, checksum: str = CHECKSUM) -> SimpleNamespace:
return SimpleNamespace(
id="source-snapshot-1",
snapshot_key="2026-08-01",
checksum_sha256=checksum,
freshness_status="current",
ingest_status="ingested",
)
def governed_dataset(
*,
role: str,
registry: SimpleNamespace | None | object = UNSET,
snapshot: SimpleNamespace | None | object = UNSET,
**overrides: object,
) -> SimpleNamespace:
dataset_type = "raster" if role == "raster" else "vector"
values: dict[str, object] = {
"id": f"dataset-{role}",
"dataset_type": dataset_type,
"dataset_role": "source" if role == "raster" else "reference",
"source": "governed_import",
"source_name": "governed-source",
"checksum_sha256": CHECKSUM,
"data_contract_key": f"{role}-contract",
"data_contract_version": "1.0.0",
"validation_status": "passed",
"provenance_status": "complete",
"lineage_status": "not_applicable",
"quarantine_status": "not_quarantined",
"status": "ready",
"metadata_json": {},
"provenance_metadata": {},
"source_registry": source_registry() if registry is UNSET else registry,
"source_snapshot": source_snapshot() if snapshot is UNSET else snapshot,
}
values.update(overrides)
return SimpleNamespace(**values)
def test_governed_authoritative_pair_is_eligible_for_operational_training() -> None:
raster = governed_dataset(
role="raster",
registry=source_registry(ground_truth_allowed=False),
)
reference = governed_dataset(role="reference")
decision = MODULE.training_pair_evidence(raster=raster, reference=reference)
assert decision["eligible"] is True
assert decision["raster"]["reasons"] == []
assert decision["reference"]["evidence"]["source_ground_truth_allowed"] is True
def test_operational_training_rejects_invalid_quarantined_incomplete_and_untrusted_inputs() -> None:
dataset = governed_dataset(
role="reference",
validation_status="failed",
provenance_status="incomplete",
lineage_status="incomplete",
quarantine_status="quarantined",
source_registry=source_registry(
classification="contextual",
training_allowed=False,
ground_truth_allowed=False,
),
)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert set(decision.reasons) >= {
"validation_failed",
"dataset_quarantined",
"provenance_not_complete",
"lineage_not_complete",
"source_not_allowed_for_training",
"reference_source_not_authoritative",
"reference_source_not_ground_truth_allowed",
}
def test_operational_training_rejects_a_due_source_snapshot() -> None:
snapshot = source_snapshot()
snapshot.freshness_status = "due"
dataset = governed_dataset(role="reference", snapshot=snapshot)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert "source_snapshot_freshness_not_approved" in decision.reasons
def test_osm_like_context_is_never_accepted_as_building_ground_truth() -> None:
dataset = governed_dataset(
role="reference",
source_name="osm",
source_registry=source_registry(
classification="contextual",
training_allowed=False,
ground_truth_allowed=False,
),
)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert "source_not_allowed_for_training" in decision.reasons
assert "reference_source_not_authoritative" in decision.reasons
def test_regional_building_sources_pending_primary_authority_cannot_enter_training_labels() -> None:
for source_key in ("spw_picc", "urbis"):
dataset = governed_dataset(
role="reference",
source_name=source_key,
source_registry=source_registry(
allowed_tasks=["building_validation", "building_labels"],
building_validation_authority="regional_primary_pending_contract",
),
)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert "reference_building_validation_not_primary" in decision.reasons
def test_authoritative_source_without_building_validation_task_cannot_be_used_as_a_label_reference() -> None:
dataset = governed_dataset(
role="reference",
source_registry=source_registry(
allowed_tasks=["elevation_validation"],
building_validation_authority="corroborative",
),
)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert set(decision.reasons) >= {
"reference_source_not_approved_for_building_validation",
"reference_building_validation_not_primary",
}
def test_dataset_and_snapshot_registry_bindings_cannot_be_forged() -> None:
snapshot = source_snapshot()
snapshot.source_registry_id = "different-registry"
dataset = governed_dataset(
role="reference",
registry=source_registry(),
snapshot=snapshot,
source_registry_id="different-registry",
source_snapshot_id="different-snapshot",
)
decision = MODULE.evaluate_dataset_training_eligibility(dataset, role="reference")
assert decision.eligible is False
assert set(decision.reasons) >= {
"dataset_source_registry_binding_mismatch",
"dataset_source_snapshot_binding_mismatch",
"source_snapshot_registry_mismatch",
}
def test_fixture_mode_only_relaxes_legacy_provenance_for_explicit_fixtures() -> None:
fixture = governed_dataset(
role="reference",
source="fixture",
source_name="fixture",
validation_status=None,
provenance_status="incomplete",
lineage_status="incomplete",
data_contract_key=None,
data_contract_version=None,
checksum_sha256=None,
source_registry=None,
source_snapshot=None,
metadata_json={"fixture": True},
)
unmarked = governed_dataset(
role="reference",
validation_status=None,
provenance_status="incomplete",
lineage_status="incomplete",
source_registry=None,
source_snapshot=None,
)
assert MODULE.evaluate_dataset_training_eligibility(
fixture,
role="reference",
fixture_mode=True,
).eligible is True
rejected = MODULE.evaluate_dataset_training_eligibility(
unmarked,
role="reference",
fixture_mode=True,
)
assert rejected.eligible is False
assert "fixture_mode_requires_explicit_fixture" in rejected.reasons
def test_fixture_mode_never_allows_failed_validation_or_quarantine() -> None:
fixture = governed_dataset(
role="raster",
source="fixture",
source_name="fixture",
validation_status="failed",
quarantine_status="quarantined",
source_registry=None,
source_snapshot=None,
)
decision = MODULE.evaluate_dataset_training_eligibility(fixture, role="raster", fixture_mode=True)
assert decision.eligible is False
assert set(decision.reasons) >= {"validation_failed", "dataset_quarantined"}
def test_manifest_gate_rejects_missing_or_tampered_pair_decisions() -> None:
raster = governed_dataset(
role="raster",
registry=source_registry(ground_truth_allowed=False),
)
reference = governed_dataset(role="reference")
pair = MODULE.training_pair_evidence(raster=raster, reference=reference)
manifest = {
"samples": [{"sample_slug": "governed", "training_eligibility": pair}],
"training_eligibility": {
"policy_version": MODULE.TRAINING_ELIGIBILITY_POLICY_VERSION,
"status": "eligible",
"fixture_mode": False,
},
}
assert MODULE.manifest_training_eligibility_failures(manifest) == []
tampered = {
**manifest,
"samples": [{"sample_slug": "governed", "training_eligibility": {**pair, "eligible": False}}],
}
failures = MODULE.manifest_training_eligibility_failures(tampered)
assert "governed:training_pair_not_eligible" in failures
assert MODULE.manifest_training_eligibility_failures({"samples": []}) == [
"manifest_training_eligibility_missing"
]
def test_frozen_manifest_gate_detects_checksum_tampering(tmp_path: Path) -> None:
raster = governed_dataset(
role="raster",
registry=source_registry(ground_truth_allowed=False),
)
reference = governed_dataset(role="reference")
pair = MODULE.training_pair_evidence(raster=raster, reference=reference)
manifest_path = tmp_path / "operator_samples_manifest.json"
manifest_path.write_text(
json.dumps(
{
"immutable": True,
"training_eligibility": {
"policy_version": MODULE.TRAINING_ELIGIBILITY_POLICY_VERSION,
"status": "eligible",
"fixture_mode": False,
},
"samples": [{"sample_slug": "governed", "training_eligibility": pair}],
}
),
encoding="utf-8",
)
(tmp_path / "corpus-freeze.json").write_text(
json.dumps(
{
"schema_version": 2,
"manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
"immutable": True,
"training_eligibility_policy": MODULE.TRAINING_ELIGIBILITY_POLICY_VERSION,
"fixture_mode": False,
}
),
encoding="utf-8",
)
assert MODULE.frozen_manifest_training_eligibility_failures(manifest_path) == []
manifest_path.write_text(manifest_path.read_text(encoding="utf-8") + "\n", encoding="utf-8")
assert "corpus_manifest_checksum_mismatch" in MODULE.frozen_manifest_training_eligibility_failures(
manifest_path
)
def test_live_manifest_gate_revokes_a_frozen_pair_when_an_upstream_dataset_is_quarantined() -> None:
raster_id = uuid4()
reference_id = uuid4()
raster_registry = source_registry(ground_truth_allowed=False)
reference_registry = source_registry()
raster_snapshot = source_snapshot()
reference_snapshot = source_snapshot()
raster_snapshot.source_registry_id = raster_registry.id
reference_snapshot.source_registry_id = reference_registry.id
raster = governed_dataset(
role="raster",
id=raster_id,
source_registry=raster_registry,
source_snapshot=raster_snapshot,
source_registry_id=raster_registry.id,
source_snapshot_id=raster_snapshot.id,
)
reference = governed_dataset(
role="reference",
id=reference_id,
source_registry=reference_registry,
source_snapshot=reference_snapshot,
source_registry_id=reference_registry.id,
source_snapshot_id=reference_snapshot.id,
)
pair = MODULE.training_pair_evidence(raster=raster, reference=reference)
manifest = {
"training_eligibility": {
"policy_version": MODULE.TRAINING_ELIGIBILITY_POLICY_VERSION,
"status": "eligible",
"fixture_mode": False,
},
"samples": [
{
"sample_slug": "governed-aoi",
"raster_dataset_id": str(raster_id),
"reference_dataset_id": str(reference_id),
"training_eligibility": pair,
}
],
}
class DatasetModel:
pass
class Session:
def __init__(self) -> None:
self.closed = False
@staticmethod
def get(model, item_id):
assert model is DatasetModel
return {raster_id: raster, reference_id: reference}.get(item_id)
def close(self) -> None:
self.closed = True
assert MODULE.live_manifest_training_eligibility_failures(
manifest,
session_factory=Session,
dataset_model=DatasetModel,
) == []
raster.quarantine_status = "quarantined"
failures = MODULE.live_manifest_training_eligibility_failures(
manifest,
session_factory=Session,
dataset_model=DatasetModel,
)
assert "governed-aoi:raster_live_revoked:dataset_quarantined" in failures
@@ -0,0 +1,414 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
SCRIPT = SCRIPTS / "training_release_manifest.py"
SPEC = importlib.util.spec_from_file_location("training_release_manifest", SCRIPT)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
@pytest.fixture(autouse=True)
def _synthetic_release_uses_a_static_live_registry_spy(monkeypatch: pytest.MonkeyPatch) -> None:
"""Filesystem unit fixtures cannot resolve a real database, but must ask for it."""
original_assert = MODULE.assert_frozen_manifest_training_eligible
original_failures = MODULE.frozen_manifest_training_eligibility_failures
def static_assert(*args, **kwargs):
assert kwargs.get("verify_live") is True
kwargs["verify_live"] = False
return original_assert(*args, **kwargs)
def static_failures(*args, **kwargs):
assert kwargs.get("verify_live") is True
kwargs["verify_live"] = False
return original_failures(*args, **kwargs)
monkeypatch.setattr(MODULE, "assert_frozen_manifest_training_eligible", static_assert)
monkeypatch.setattr(MODULE, "frozen_manifest_training_eligibility_failures", static_failures)
def write_corpus_manifest(tmp_path: Path, *, fixture_mode: bool = False) -> Path:
policy = "geointel-training-source-eligibility/v1"
def pair(sample_slug: str) -> dict:
raster_id = f"dataset:raster:{sample_slug}"
reference_id = f"dataset:reference:{sample_slug}"
return {
"policy_version": policy,
"eligible": True,
"fixture_mode": fixture_mode,
"raster": {
"eligible": True,
"reasons": [],
"evidence": {
"dataset_id": raster_id,
"checksum_sha256": "a" * 64,
"source_registry_id": "registry:orthophoto",
"source_snapshot_id": "snapshot:orthophoto",
},
},
"reference": {
"eligible": True,
"reasons": [],
"evidence": {
"dataset_id": reference_id,
"checksum_sha256": "b" * 64,
"source_registry_id": "registry:grb",
"source_snapshot_id": "snapshot:grb",
},
},
}
samples = []
for sample_slug, split in (("fixture-train", "train"), ("fixture-val", "val")):
samples.append(
{
"sample_slug": sample_slug,
"split": split,
"raster_dataset_id": f"dataset:raster:{sample_slug}",
"reference_dataset_id": f"dataset:reference:{sample_slug}",
"training_eligibility": pair(sample_slug),
}
)
manifest = tmp_path / "operator_samples_manifest.json"
manifest.write_text(
json.dumps(
{
"immutable": True,
"training_eligibility": {
"policy_version": policy,
"status": "eligible",
"fixture_mode": fixture_mode,
},
"samples": samples,
}
),
encoding="utf-8",
)
(tmp_path / "corpus-freeze.json").write_text(
json.dumps(
{
"schema_version": 2,
"immutable": True,
"fixture_mode": fixture_mode,
"training_eligibility_policy": policy,
"manifest_sha256": sha256(manifest),
}
),
encoding="utf-8",
)
return manifest
def write_yolo_dataset(tmp_path: Path, *, empty_train_label: bool = False) -> Path:
dataset_root = tmp_path / "dataset"
for split, sample_slug in (("train", "fixture-train"), ("val", "fixture-val")):
image = dataset_root / "images" / split / f"{sample_slug}.png"
label = dataset_root / "labels" / split / f"{sample_slug}.txt"
image.parent.mkdir(parents=True, exist_ok=True)
label.parent.mkdir(parents=True, exist_ok=True)
image.write_bytes(f"{split}-image".encode("utf-8"))
label.write_text("" if split == "train" and empty_train_label else "0 0.5 0.5 0.2 0.2\n", encoding="utf-8")
yaml_path = dataset_root / "dataset.yaml"
yaml_path.write_text(
f"path: {dataset_root}\ntrain: images/train\nval: images/val\nnames:\n 0: building\n",
encoding="utf-8",
)
return yaml_path
def write_accepted_review_audit(tmp_path: Path, corpus_manifest: Path) -> Path:
artifacts = {}
for sample_slug in ("fixture-train", "fixture-val"):
artifact = tmp_path / f"{sample_slug}-contact-sheet.png"
artifact.write_bytes(f"reviewed {sample_slug}".encode("utf-8"))
artifacts[sample_slug] = artifact
decisions = tmp_path / "review-decisions.json"
decisions.write_text(
json.dumps(
{
"decisions": [
{
"sample_slug": sample_slug,
"decision": "accepted",
"reviewer": "reviewer@example.test",
"reviewed_at": "2026-08-01T12:00:00+00:00",
"reviewed_artifact_path": str(artifact.resolve()),
"reviewed_artifact_sha256": sha256(artifact),
}
for sample_slug, artifact in artifacts.items()
]
}
),
encoding="utf-8",
)
evidence = {
"review_decisions_path": str(decisions.resolve()),
"review_decisions_sha256": sha256(decisions),
"required_sample_count": 2,
"accepted_sample_count": 2,
"accepted_sample_slugs": ["fixture-train", "fixture-val"],
"reviewer_ids": ["reviewer@example.test"],
"reviewed_at_by_sample": {
"fixture-train": "2026-08-01T12:00:00+00:00",
"fixture-val": "2026-08-01T12:00:00+00:00",
},
"reviewed_artifact_path_by_sample": {
sample_slug: str(artifact.resolve()) for sample_slug, artifact in artifacts.items()
},
"reviewed_artifact_sha256_by_sample": {
sample_slug: sha256(artifact) for sample_slug, artifact in artifacts.items()
},
}
audit = tmp_path / "belgium-building-corpus-audit.json"
audit.write_text(
json.dumps(
{
"status": "ok",
"manifest_immutable": True,
"spatial_leakage_status": "ok",
"review_complete": True,
"corpus_manifest_path": str(corpus_manifest.resolve()),
"corpus_manifest_sha256": sha256(corpus_manifest),
"human_review_evidence": evidence,
}
),
encoding="utf-8",
)
return audit
def test_valid_operational_release_binds_yaml_assets_corpus_and_human_review(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
paths = MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
assert all(path.is_file() for path in paths.values())
assert MODULE.training_release_failures(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
) == []
def test_tile_summary_must_be_an_exact_view_of_the_live_verified_release(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
paths = MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
assets = json.loads(paths["asset_manifest"].read_text(encoding="utf-8"))
summary = yaml_path.parent / "yolo_tile_dataset_summary.json"
summary.write_text(
json.dumps(
{
"dataset_yaml": str(yaml_path.resolve()),
"training_release_manifest": str(paths["release_manifest"].resolve()),
"training_release_manifest_sha256": sha256(paths["release_manifest"]),
"training_asset_manifest": str(paths["asset_manifest"].resolve()),
"source_manifest_sha256": sha256(corpus_manifest),
"tiles": [
{
"split": entry["split"],
"image_path": entry["image_path"],
"label_path": entry["label_path"],
}
for entry in assets["entries"]
],
}
),
encoding="utf-8",
)
MODULE.assert_yolo_summary_bound_to_training_release(
summary_path=summary,
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
)
payload = json.loads(summary.read_text(encoding="utf-8"))
payload["tiles"] = payload["tiles"][:1]
summary.write_text(json.dumps(payload), encoding="utf-8")
with pytest.raises(MODULE.TrainingReleaseError, match="complete immutable view"):
MODULE.assert_yolo_summary_bound_to_training_release(
summary_path=summary,
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
)
def test_unbound_or_changed_yaml_is_rejected_before_training(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
assert MODULE.training_release_failures(train_yaml=yaml_path) == ["training_release_manifest_missing"]
MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
yaml_path.write_text(yaml_path.read_text(encoding="utf-8") + "# tampered\n", encoding="utf-8")
assert "training_release_yaml_checksum_mismatch" in MODULE.training_release_failures(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
)
def test_changed_label_asset_is_rejected_even_when_yaml_bytes_are_unchanged(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
label = yaml_path.parent / "labels" / "train" / "fixture-train.txt"
label.write_text("0 0.4 0.4 0.2 0.2\n", encoding="utf-8")
failures = MODULE.training_release_failures(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
)
assert "training_release_asset_manifest_content_mismatch" in failures
def test_operational_release_requires_complete_accepted_human_review(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
incomplete_audit = tmp_path / "audit.json"
incomplete_audit.write_text(
json.dumps(
{
"status": "needs_human_review",
"manifest_immutable": True,
"spatial_leakage_status": "ok",
"review_complete": False,
}
),
encoding="utf-8",
)
try:
MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=incomplete_audit,
)
except MODULE.TrainingReleaseError as exc:
assert "review_complete_not_true" in str(exc)
assert "accepted_human_review_evidence_missing" in str(exc)
else:
raise AssertionError("operational release accepted an incomplete human review")
def test_operational_release_rejects_tampered_accepted_review_artifact(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
artifact = tmp_path / "fixture-train-contact-sheet.png"
artifact.write_bytes(b"changed after review")
try:
MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
except MODULE.TrainingReleaseError as exc:
assert "reviewed_artifact_checksum_mismatch" in str(exc)
else:
raise AssertionError("tampered human-review artifact was accepted")
def test_fixture_relaxation_requires_explicit_fixture_corpus_and_is_not_operational(tmp_path: Path) -> None:
fixture_manifest = write_corpus_manifest(tmp_path, fixture_mode=True)
yaml_path = write_yolo_dataset(tmp_path)
MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=fixture_manifest,
fixture_mode=True,
)
assert MODULE.training_release_failures(
train_yaml=yaml_path,
corpus_manifest=fixture_manifest,
fixture_mode=True,
) == []
assert "training_release_fixture_mode_mismatch" in MODULE.training_release_failures(
train_yaml=yaml_path,
corpus_manifest=fixture_manifest,
fixture_mode=False,
)
def test_release_contracts_a_reviewed_empty_label_as_explicit_pure_background(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path, empty_train_label=True)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
paths = MODULE.create_training_release_manifest(
train_yaml=yaml_path,
corpus_manifest=corpus_manifest,
review_audit_path=review_audit,
)
labels = json.loads(paths["label_contract_manifest"].read_text(encoding="utf-8"))
assert labels["counts"]["pure_background"] == 1
assert MODULE.training_release_failures(train_yaml=yaml_path, corpus_manifest=corpus_manifest) == []
def test_empty_label_without_accepted_sample_review_cannot_become_a_background_negative(tmp_path: Path) -> None:
corpus_manifest = write_corpus_manifest(tmp_path)
yaml_path = write_yolo_dataset(tmp_path, empty_train_label=True)
review_audit = write_accepted_review_audit(tmp_path, corpus_manifest)
audit_payload = json.loads(review_audit.read_text(encoding="utf-8"))
evidence = audit_payload["human_review_evidence"]
evidence["accepted_sample_slugs"] = ["fixture-val"]
review_audit.write_text(json.dumps(audit_payload), encoding="utf-8")
review = {
"status": "accepted",
"fixture_only": False,
"review_complete": True,
"evidence": evidence,
}
try:
MODULE.build_training_label_contract_manifest(
corpus_manifest_path=corpus_manifest,
asset_manifest=MODULE.build_yolo_asset_manifest(yaml_path),
review=review,
fixture_mode=False,
)
except MODULE.TrainingReleaseError as exc:
assert "Pure-background label sample was not accepted by review" in str(exc)
else:
raise AssertionError("unreviewed empty label was accepted as a background negative")
@@ -1,7 +1,6 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from uuid import UUID, uuid4
from app.core.errors import AppError