Files
geointel/backend/app/services/dataset_consumption_gate_service.py
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

441 lines
20 KiB
Python

"""Fail-closed provenance gates at data-consumption boundaries.
Import validation protects newly staged assets, but a persisted record can
subsequently become quarantined or have its provenance marked incomplete. The
callers of this service therefore re-check the durable Dataset state directly
before production inference, QA, derived processing, export, or authoritative
coverage reporting.
The only legacy relaxation is deliberately narrow: an *explicitly tagged*
fixture with no Phase-2 state can be used for fixture QA. A caller-provided
``fixture_mode`` flag alone never creates that trust claim. Fixture data can
never become a production inference, derived-processing, export or
authoritative-coverage input, and it never relaxes a recorded failed,
incomplete, or quarantined state.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import re
from typing import Any, Literal
from sqlalchemy import inspect as sa_inspect
from app.core.errors import AppError
from app.models import Dataset
DatasetConsumptionPurpose = Literal[
"production_inference",
"quality_assessment",
"reference_validation",
"derived_processing",
"authoritative_coverage",
"export",
]
_FIXTURE_SOURCE_KEYS = {"fixture", "test", "test_fixture", "test-fixture", "unit-test-fixture"}
_UNTRUSTED_SOURCE_KEYS = {"manual", "fixture", "experimental", "legacy_unknown"}
_CHECKSUM_SHA256 = re.compile(r"^[0-9a-f]{64}$", re.IGNORECASE)
_CONSUMABLE_SNAPSHOT_FRESHNESS = {"current", "not_applicable"}
_VALID_PURPOSES = {
"production_inference",
"quality_assessment",
"reference_validation",
"derived_processing",
"authoritative_coverage",
"export",
}
@dataclass(frozen=True)
class DatasetConsumptionDecision:
"""Auditable decision returned by the consumption gate."""
eligible: bool
purpose: DatasetConsumptionPurpose
fixture_legacy_exception: bool
reasons: tuple[str, ...]
evidence: dict[str, Any]
class DatasetConsumptionGate:
"""Evaluate durable provenance before a Dataset is consumed downstream."""
@staticmethod
def _value(dataset: Any, field: str, default: Any = None) -> Any:
if isinstance(dataset, Mapping):
return dataset.get(field, default)
return getattr(dataset, field, default)
@staticmethod
def _mapping(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, Mapping) else {}
@classmethod
def _normalise(cls, value: Any) -> str:
return str(value or "").strip().lower()
@classmethod
def is_explicit_fixture(cls, dataset: Any) -> bool:
source = cls._normalise(cls._value(dataset, "source"))
source_name = cls._normalise(cls._value(dataset, "source_name"))
metadata = cls._mapping(cls._value(dataset, "metadata_json"))
source_metadata = cls._mapping(cls._value(dataset, "source_metadata"))
provenance = cls._mapping(cls._value(dataset, "provenance_metadata"))
return bool(
source in _FIXTURE_SOURCE_KEYS
or source_name in _FIXTURE_SOURCE_KEYS
or metadata.get("fixture") is True
or metadata.get("fixture_mode") is True
or source_metadata.get("fixture") is True
or source_metadata.get("fixture_mode") is True
or provenance.get("fixture") is True
or provenance.get("fixture_mode") is True
)
@classmethod
def _phase2_state_is_absent(cls, dataset: Any) -> bool:
fields = (
"data_contract_key",
"data_contract_version",
"validation_status",
"provenance_status",
"lineage_status",
"quarantine_status",
"source_registry_id",
"source_snapshot_id",
)
return all(cls._value(dataset, field) in {None, ""} for field in fields)
@staticmethod
def _is_transient_orm_dataset(dataset: Any) -> bool:
"""Recognize only unpersisted ORM fixtures, never database rows."""
if not isinstance(dataset, Dataset):
return False
try:
return bool(sa_inspect(dataset).transient)
except Exception: # pragma: no cover - defensive for unusual test doubles
return False
@classmethod
def _evidence(
cls,
dataset: Any,
purpose: DatasetConsumptionPurpose,
reference_task: str | None = None,
) -> dict[str, Any]:
source_registry = cls._value(dataset, "source_registry")
source_snapshot = cls._value(dataset, "source_snapshot")
source_policy = cls._mapping(cls._value(source_registry, "usage_policy_json"))
validation_authority = cls._mapping(source_policy.get("validation_authority"))
reference_approvals = cls._mapping(source_policy.get("reference_validation_approvals"))
return {
"dataset_id": str(cls._value(dataset, "id") or ""),
"purpose": purpose,
"dataset_status": cls._normalise(cls._value(dataset, "status")),
"source": cls._normalise(cls._value(dataset, "source")),
"source_name": cls._normalise(cls._value(dataset, "source_name")),
"data_contract_key": cls._value(dataset, "data_contract_key"),
"data_contract_version": cls._value(dataset, "data_contract_version"),
"validation_status": cls._normalise(cls._value(dataset, "validation_status")),
"provenance_status": cls._normalise(cls._value(dataset, "provenance_status")),
"lineage_status": cls._normalise(cls._value(dataset, "lineage_status")),
"quarantine_status": cls._normalise(cls._value(dataset, "quarantine_status")),
"checksum_sha256": cls._value(dataset, "checksum_sha256"),
"source_registry_id": str(cls._value(dataset, "source_registry_id") or ""),
"source_snapshot_id": str(cls._value(dataset, "source_snapshot_id") or ""),
"source_classification": cls._normalise(cls._value(source_registry, "classification")),
"source_key": cls._normalise(cls._value(source_registry, "source_key")),
"source_ground_truth_allowed": source_policy.get("ground_truth_allowed") is True,
"source_validation_authority": validation_authority,
"source_reference_validation_approvals": reference_approvals,
"source_authority_scope": cls._mapping(cls._value(source_registry, "authority_scope_json")),
"reference_task": cls._normalise(reference_task),
"snapshot_source_registry_id": str(cls._value(source_snapshot, "source_registry_id") or ""),
"snapshot_ingest_status": cls._normalise(cls._value(source_snapshot, "ingest_status")),
"snapshot_freshness_status": cls._normalise(cls._value(source_snapshot, "freshness_status")),
"snapshot_checksum_sha256": cls._value(source_snapshot, "checksum_sha256"),
}
@classmethod
def evaluate(
cls,
dataset: Any,
*,
purpose: DatasetConsumptionPurpose,
fixture_mode: bool = False,
reference_task: str | None = None,
) -> DatasetConsumptionDecision:
"""Return a stable decision without mutating the Dataset.
``fixture_mode`` can be supplied only by an explicitly fixture-only
caller. It is evidence for a QA fixture path, never a relaxation for
a production boundary or an explicit unsafe state.
"""
if purpose not in _VALID_PURPOSES:
raise ValueError(f"Unsupported dataset-consumption purpose: {purpose}")
evidence = cls._evidence(dataset, purpose, reference_task)
reasons: list[str] = []
explicit_fixture = cls.is_explicit_fixture(dataset)
phase2_absent = cls._phase2_state_is_absent(dataset)
# These are irrevocable safety states. They are checked before a
# fixture exception, so fixture rows cannot hide a bad recorded state.
if evidence["dataset_status"] in {"failed", "quarantined"}:
reasons.append("dataset_status_unsafe")
if evidence["quarantine_status"] == "quarantined":
reasons.append("dataset_quarantined")
if evidence["validation_status"] == "failed":
reasons.append("validation_failed")
if evidence["provenance_status"] == "incomplete":
reasons.append("provenance_incomplete")
if evidence["lineage_status"] == "incomplete":
reasons.append("lineage_incomplete")
if evidence["snapshot_ingest_status"] in {"failed", "quarantined"}:
reasons.append("source_snapshot_unsafe")
# A source family can be authoritative while an individual snapshot
# remains too old or insufficiently described to trust. Historical
# data that is intentionally valid needs an explicit
# ``not_applicable`` contract policy; an omitted, due or stale status
# cannot silently enter a production boundary.
if evidence["source_snapshot_id"] and evidence["snapshot_freshness_status"] not in _CONSUMABLE_SNAPSHOT_FRESHNESS:
reasons.append("source_snapshot_freshness_not_eligible")
if reasons:
return DatasetConsumptionDecision(
eligible=False,
purpose=purpose,
fixture_legacy_exception=False,
reasons=tuple(sorted(set(reasons))),
evidence=evidence,
)
# Fixtures are evidence for tests and QA only. They cannot become
# production inference, derived processing or export inputs merely by
# presenting a fixture flag at a public service boundary.
if phase2_absent and explicit_fixture:
if purpose == "quality_assessment":
return DatasetConsumptionDecision(
eligible=True,
purpose=purpose,
fixture_legacy_exception=True,
reasons=(),
evidence=evidence,
)
if purpose == "authoritative_coverage":
reasons.append("fixture_not_authoritative_coverage")
else:
reasons.append("fixture_qa_only")
elif phase2_absent and fixture_mode:
# `fixture_mode` is a caller flag, not a source trust claim. A
# manual/unknown production dataset must never self-designate as a
# fixture merely by supplying this parameter.
reasons.append("fixture_source_required")
# Existing service tests construct transient SQLAlchemy Dataset objects
# directly rather than retrieving a persisted row. A production
# `db.get()` result is persistent and never enters this branch. This
# compatibility path is intentionally unavailable to coverage, where
# a fixture must never appear authoritative.
if (
phase2_absent
and not reasons
and cls._is_transient_orm_dataset(dataset)
and purpose == "quality_assessment"
):
return DatasetConsumptionDecision(
eligible=True,
purpose=purpose,
fixture_legacy_exception=True,
reasons=(),
evidence=evidence,
)
# QA unit tests intentionally use projection objects
# instead of persisted ORM Datasets. Those projections cannot enter an
# application API boundary; keep the exception isolated to read-only
# candidate verification. Inference, reference validation, derived
# processing, export and coverage never accept a projection.
if phase2_absent and not reasons and not isinstance(dataset, Dataset) and purpose == "quality_assessment":
return DatasetConsumptionDecision(
eligible=True,
purpose=purpose,
fixture_legacy_exception=True,
reasons=(),
evidence=evidence,
)
if phase2_absent:
reasons.append("phase2_provenance_missing")
if evidence["dataset_status"] != "ready":
reasons.append("dataset_not_ready")
if evidence["validation_status"] != "passed":
reasons.append("validation_not_passed")
if evidence["provenance_status"] != "complete":
reasons.append("provenance_not_complete")
if evidence["lineage_status"] not in {"complete", "not_applicable"}:
reasons.append("lineage_not_complete")
if evidence["quarantine_status"] != "not_quarantined":
reasons.append("quarantine_status_not_clear")
if not evidence["data_contract_key"] or not evidence["data_contract_version"]:
reasons.append("data_contract_not_versioned")
if not _CHECKSUM_SHA256.fullmatch(str(evidence["checksum_sha256"] or "")):
reasons.append("dataset_checksum_invalid")
if not evidence["source_registry_id"]:
reasons.append("source_registry_missing")
if not evidence["source_snapshot_id"]:
reasons.append("source_snapshot_missing")
if evidence["source_registry_id"] and not evidence["source_classification"]:
reasons.append("source_registry_unresolved")
if evidence["source_snapshot_id"] and evidence["snapshot_ingest_status"] != "ingested":
reasons.append("source_snapshot_not_ingested")
if evidence["source_snapshot_id"] and not _CHECKSUM_SHA256.fullmatch(
str(evidence["snapshot_checksum_sha256"] or "")
):
reasons.append("source_snapshot_checksum_invalid")
if (
evidence["source_registry_id"]
and evidence["source_snapshot_id"]
and evidence["snapshot_source_registry_id"]
and evidence["source_registry_id"] != evidence["snapshot_source_registry_id"]
):
reasons.append("source_snapshot_registry_mismatch")
if (
_CHECKSUM_SHA256.fullmatch(str(evidence["checksum_sha256"] or ""))
and _CHECKSUM_SHA256.fullmatch(str(evidence["snapshot_checksum_sha256"] or ""))
and str(evidence["checksum_sha256"]).lower() != str(evidence["snapshot_checksum_sha256"]).lower()
):
reasons.append("source_snapshot_checksum_mismatch")
if evidence["source_key"] and evidence["source_name"] and evidence["source_key"] != evidence["source_name"]:
reasons.append("source_registry_identity_mismatch")
# Contextual, corroborative, authoritative and properly derived
# sources can serve their declared non-ground-truth roles once the
# full governed contract passes. Experimental/manual sources cannot
# cross a production boundary; only an explicitly marked fixture may
# participate in candidate QA.
experimental_source = (
evidence["source_classification"] == "experimental"
or evidence["source_key"] in _UNTRUSTED_SOURCE_KEYS
)
if experimental_source:
if purpose == "quality_assessment" and explicit_fixture:
pass
elif purpose == "quality_assessment":
reasons.append("experimental_source_requires_fixture_qa")
else:
reasons.append("experimental_source_not_allowed_for_purpose")
if purpose == "authoritative_coverage":
if evidence["source_classification"] != "authoritative":
reasons.append("coverage_source_not_authoritative")
if evidence["source_key"] and evidence["source_name"] and evidence["source_key"] != evidence["source_name"]:
reasons.append("coverage_source_identity_mismatch")
if purpose == "reference_validation":
if cls._normalise(cls._value(dataset, "dataset_role")) != "reference":
reasons.append("reference_dataset_role_required")
if evidence["source_classification"] != "authoritative":
reasons.append("reference_source_not_authoritative")
if evidence["source_ground_truth_allowed"] is not True:
reasons.append("reference_source_not_ground_truth_allowed")
if not evidence["reference_task"]:
reasons.append("reference_validation_task_required")
elif not cls._reference_task_is_approved(evidence):
reasons.append("reference_task_authority_not_approved")
return DatasetConsumptionDecision(
eligible=not reasons,
purpose=purpose,
fixture_legacy_exception=False,
reasons=tuple(sorted(set(reasons))),
evidence=evidence,
)
@classmethod
def assert_eligible(
cls,
dataset: Any,
*,
purpose: DatasetConsumptionPurpose,
fixture_mode: bool = False,
reference_task: str | None = None,
) -> DatasetConsumptionDecision:
decision = cls.evaluate(
dataset,
purpose=purpose,
fixture_mode=fixture_mode,
reference_task=reference_task,
)
if decision.eligible:
return decision
code = "DATASET_QUARANTINED" if any(
reason in {"dataset_status_unsafe", "dataset_quarantined", "validation_failed", "source_snapshot_unsafe"}
for reason in decision.reasons
) else "DATASET_PROVENANCE_INCOMPLETE"
raise AppError(
code=code,
message="Dataset cannot be consumed until its provenance and validation gates are satisfied.",
status_code=409,
details={
"dataset_id": decision.evidence["dataset_id"],
"purpose": purpose,
"reasons": list(decision.reasons),
"fixture_legacy_exception": decision.fixture_legacy_exception,
},
)
@classmethod
def eligible_for_authoritative_coverage(cls, dataset: Any) -> bool:
"""Return false instead of raising so coverage can report a gap safely."""
return cls.evaluate(dataset, purpose="authoritative_coverage").eligible
@staticmethod
def _reference_task_is_approved(evidence: Mapping[str, Any]) -> bool:
"""Require task-specific primary authority or an explicit zone approval.
``classification=authoritative`` is deliberately not a blanket
permission to serve as building truth. A source may be authoritative
for an address lifecycle or elevation product while remaining only
corroborative for footprint QA. A regional product that is marked
``*_pending_contract`` similarly remains blocked until an operator
records a narrow product-and-zone approval in its server-owned policy.
"""
task = str(evidence.get("reference_task") or "").strip().lower()
authority = DatasetConsumptionGate._normalise(
DatasetConsumptionGate._mapping(evidence.get("source_validation_authority")).get(task)
)
if authority == "primary":
return True
if authority not in {"approved", "approved_product_zone"}:
return False
approvals = DatasetConsumptionGate._mapping(evidence.get("source_reference_validation_approvals"))
approval = DatasetConsumptionGate._mapping(approvals.get(task))
if approval.get("approved") is not True:
return False
source_key = str(evidence.get("source_key") or "").strip().lower()
source_scope = DatasetConsumptionGate._mapping(evidence.get("source_authority_scope"))
source_zone = str(source_scope.get("zone") or source_scope.get("scope") or "").strip()
approved_keys = approval.get("source_keys")
approved_zones = approval.get("zones")
if not isinstance(approved_keys, list) or source_key not in {
str(value).strip().lower() for value in approved_keys
}:
return False
if not isinstance(approved_zones, list) or source_zone not in {
str(value).strip() for value in approved_zones
}:
return False
return True