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
+47 -1
View File
@@ -9,6 +9,7 @@ new provider data.
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
@@ -16,6 +17,19 @@ import sys
from pathlib import Path
from typing import Any, Iterable
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from training_dataset_eligibility import ( # noqa: E402
TrainingEligibilityError,
assert_frozen_manifest_training_eligible,
)
from training_release_manifest import ( # noqa: E402
TrainingReleaseError,
create_training_release_manifest,
)
DEFAULT_MANIFEST_PATH = Path("/app/storage/operator-data/operator_samples_manifest.json")
DEFAULT_OUTPUT_DIR = Path("/app/storage/operator-data/yolo-building-dataset")
@@ -24,6 +38,14 @@ Transformer: Any = None
Image: Any = None
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Export operator real-data samples to a YOLO detection dataset.",
@@ -45,6 +67,12 @@ def parse_args() -> argparse.Namespace:
default=os.environ.get("OPERATOR_YOLO_VAL_SAMPLES", "turnhout"),
help="Comma/space separated sample slugs assigned to validation. Defaults to turnhout.",
)
parser.add_argument(
"--review-audit",
type=Path,
required=True,
help="Passed corpus audit with accepted human-review evidence for this frozen source manifest.",
)
parser.add_argument(
"--force",
action="store_true",
@@ -218,12 +246,16 @@ def ensure_yolo_directories(output_dir: Path) -> None:
def main() -> int:
args = parse_args()
manifest = json.loads(args.manifest_path.read_text(encoding="utf-8-sig"))
try:
assert_frozen_manifest_training_eligible(args.manifest_path, verify_live=True)
except TrainingEligibilityError as exc:
raise SystemExit(str(exc)) from exc
ensure_dependencies()
if args.force and args.output_dir.exists():
shutil.rmtree(args.output_dir)
args.output_dir.mkdir(parents=True, exist_ok=True)
ensure_yolo_directories(args.output_dir)
manifest = json.loads(args.manifest_path.read_text(encoding="utf-8-sig"))
samples = manifest.get("samples") or []
if not samples:
raise SystemExit("Operator sample manifest contains no samples")
@@ -234,12 +266,26 @@ def main() -> int:
if not any(item["split"] == "val" for item in exported):
raise SystemExit("YOLO dataset export produced no validation samples")
dataset_yaml = write_dataset_yaml(args.output_dir)
try:
release_paths = create_training_release_manifest(
train_yaml=dataset_yaml,
corpus_manifest=args.manifest_path,
review_audit_path=args.review_audit,
)
except TrainingReleaseError as exc:
raise SystemExit(str(exc)) from exc
summary = {
"status": "ok",
"dataset_yaml": str(dataset_yaml),
"training_release_manifest": str(release_paths["release_manifest"]),
"training_release_manifest_sha256": file_sha256(release_paths["release_manifest"]),
"training_release_freeze": str(release_paths["release_freeze"]),
"training_asset_manifest": str(release_paths["asset_manifest"]),
"output_dir": str(args.output_dir),
"class_names": ["building"],
"sample_count": len(exported),
"source_manifest": str(args.manifest_path),
"source_manifest_sha256": file_sha256(args.manifest_path),
"train_sample_count": sum(1 for item in exported if item["split"] == "train"),
"val_sample_count": sum(1 for item in exported if item["split"] == "val"),
"label_count": sum(item["label_count"] for item in exported),