feat(provenance): govern source snapshots and data inputs
This commit is contained in:
@@ -26,6 +26,10 @@ from app.db.session import SessionLocal # noqa: E402
|
||||
from app.models import Dataset # noqa: E402
|
||||
|
||||
from normalize_belgium_building_labels import normalize # noqa: E402
|
||||
from training_dataset_eligibility import ( # noqa: E402
|
||||
TRAINING_ELIGIBILITY_POLICY_VERSION,
|
||||
training_pair_evidence,
|
||||
)
|
||||
|
||||
REGION_SOURCES = {
|
||||
"flanders": ({"digitaal_vlaanderen_orthophoto"}, "grb"),
|
||||
@@ -52,7 +56,13 @@ def _dataset_path(dataset: Dataset) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def _validate_pair(sample: dict[str, Any], raster: Dataset, reference: Dataset) -> tuple[str, str]:
|
||||
def _validate_pair(
|
||||
sample: dict[str, Any],
|
||||
raster: Dataset,
|
||||
reference: Dataset,
|
||||
*,
|
||||
fixture_mode: bool,
|
||||
) -> tuple[str, str, dict[str, Any]]:
|
||||
region = str(sample.get("region") or "").lower()
|
||||
if region not in REGION_SOURCES:
|
||||
raise SystemExit(f"Unsupported region for {sample.get('sample_slug')}: {region}")
|
||||
@@ -66,7 +76,23 @@ def _validate_pair(sample: dict[str, Any], raster: Dataset, reference: Dataset)
|
||||
raise SystemExit(f"Unsupported split for {sample['sample_slug']}: {split}")
|
||||
if raster.status != "ready" or reference.status != "ready":
|
||||
raise SystemExit(f"Dataset pair is not ready for {sample['sample_slug']}")
|
||||
return region, expected_reference
|
||||
eligibility = training_pair_evidence(
|
||||
raster=raster,
|
||||
reference=reference,
|
||||
fixture_mode=fixture_mode,
|
||||
)
|
||||
if not eligibility["eligible"]:
|
||||
reasons = sorted(
|
||||
{
|
||||
reason
|
||||
for role in ("raster", "reference")
|
||||
for reason in eligibility[role]["reasons"]
|
||||
}
|
||||
)
|
||||
raise SystemExit(
|
||||
f"Dataset pair is not eligible for training for {sample['sample_slug']}: {', '.join(reasons)}"
|
||||
)
|
||||
return region, expected_reference, eligibility
|
||||
|
||||
|
||||
def audit_spatial_leakage(samples: list[dict[str, Any]], buffer_m: float = 64.0) -> dict[str, Any]:
|
||||
@@ -104,6 +130,14 @@ def main() -> int:
|
||||
parser.add_argument("--min-label-px", type=float, default=3.0)
|
||||
parser.add_argument("--merge-touching-roofs", action="store_true")
|
||||
parser.add_argument("--freeze", action="store_true")
|
||||
parser.add_argument(
|
||||
"--fixture-mode",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Allow only explicitly marked fixture datasets with legacy provenance. "
|
||||
"Never use this mode for an operational corpus."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
spec = json.loads(args.spec.read_text(encoding="utf-8-sig"))
|
||||
@@ -129,7 +163,12 @@ def main() -> int:
|
||||
reference = db.get(Dataset, UUID(str(sample["reference_dataset_id"])))
|
||||
if raster is None or reference is None:
|
||||
raise SystemExit(f"Persisted Dataset pair not found for {slug}")
|
||||
region, reference_source = _validate_pair(sample, raster, reference)
|
||||
region, reference_source, eligibility = _validate_pair(
|
||||
sample,
|
||||
raster,
|
||||
reference,
|
||||
fixture_mode=args.fixture_mode,
|
||||
)
|
||||
raster_source = _dataset_path(raster)
|
||||
reference_source_path = _dataset_path(reference)
|
||||
sample_dir = pairs_dir / slug
|
||||
@@ -173,14 +212,20 @@ def main() -> int:
|
||||
"raster_sha256": sha256(raster_target),
|
||||
"reference_sha256": sha256(normalized_target),
|
||||
"label_audit_sha256": sha256(audit_target),
|
||||
"training_eligibility": eligibility,
|
||||
"bbox_epsg4326": (raster.source_metadata or {}).get("bbox_epsg4326")
|
||||
or sample.get("bbox_epsg4326"),
|
||||
}
|
||||
)
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"dataset_version": args.version,
|
||||
"immutable": bool(args.freeze),
|
||||
"training_eligibility": {
|
||||
"policy_version": TRAINING_ELIGIBILITY_POLICY_VERSION,
|
||||
"status": "eligible",
|
||||
"fixture_mode": bool(args.fixture_mode),
|
||||
},
|
||||
"samples": manifest_samples,
|
||||
}
|
||||
manifest_path = output_dir / "operator_samples_manifest.json"
|
||||
@@ -192,10 +237,13 @@ def main() -> int:
|
||||
if leakage_audit["status"] != "ok":
|
||||
raise SystemExit("Spatial split leakage audit failed")
|
||||
freeze = {
|
||||
"schema_version": 2,
|
||||
"dataset_version": args.version,
|
||||
"manifest_sha256": sha256(manifest_path),
|
||||
"sample_count": len(manifest_samples),
|
||||
"immutable": bool(args.freeze),
|
||||
"training_eligibility_policy": TRAINING_ELIGIBILITY_POLICY_VERSION,
|
||||
"fixture_mode": bool(args.fixture_mode),
|
||||
}
|
||||
(output_dir / "corpus-freeze.json").write_text(json.dumps(freeze, indent=2), encoding="utf-8")
|
||||
print(json.dumps(freeze))
|
||||
|
||||
Reference in New Issue
Block a user